File Coverage

blib/lib/Net/Async/DigitalOcean.pm
Criterion Covered Total %
statement 48 408 11.7
branch 0 122 0.0
condition 0 21 0.0
subroutine 16 105 15.2
pod 60 60 100.0
total 124 716 17.3


line stmt bran cond sub pod time code
1             package Net::Async::DigitalOcean::RateLimited;
2              
3 12     12   2546282 use strict;
  12         134  
  12         480  
4 12     12   75 use warnings;
  12         27  
  12         410  
5 12     12   66 use Data::Dumper;
  12         25  
  12         798  
6              
7 12     12   8094 use Net::Async::HTTP;
  12         1355949  
  12         716  
8 12     12   126 use parent qw( Net::Async::HTTP );
  12         27  
  12         104  
9              
10             sub prepare_request {
11 0     0     my ($elf, $req) = @_;
12             #warn "prepare $elf";
13 0           $elf->SUPER::prepare_request( $req );
14 0 0         warn $req->as_string . " >>>> DigitalOcean" if $elf->{digitalocean_trace};
15              
16 0 0         if (my $limits = $elf->{digitalocean_rate_limit}) { # if we already experienced some limit information from the server
17             #warn "rate_limit current ".Dumper $limits; #
18              
19 0   0       my $backoff = $elf->{digitalocean_rate_limit_backoff} //= 0; # default is to not wait
20              
21             my $absolute = $elf->{digitalocean_rate_limit_absolute} //= { # compile it the policy into absolute values
22             map { ( $_ =~ /(\d+)\%/
23             ? $limits->{Limit} * $1 / 100
24 0 0         : $_) => $elf->{digitalocean_rate_limit_policy}->{$_} }
25 0   0       keys %{ $elf->{digitalocean_rate_limit_policy} }
  0            
26             };
27             #warn "absolute ".Dumper $absolute;
28             #warn "remaining ".$limits->{Remaining};
29 0           foreach my $threshold ( sort keys %$absolute ) { # analyse - starting from the lowest
30             #warn "limit found $limits->{Remaining} < $threshold";
31 0 0         if ($limits->{Remaining} < $threshold) { # if we are already under that
32 0           $backoff = &{$absolute->{ $threshold }} ( $backoff ); # compute new backoff, following the expression provided
  0            
33 0 0         $backoff = 0 if $backoff < 0; # dont want to go negative here
34             #warn "\\_ NEW backoff $backoff";
35 0           last; # no further going up
36             }
37             }
38            
39 0           $elf->{digitalocean_rate_limit_backoff} = $backoff;
40             #warn "have to wait $backoff ".$elf->loop;
41 0 0         $elf->loop->delay_future( after => $backoff )->get if $backoff > 0;
42             #warn "\\_ done waiting";
43             }
44              
45 0           return $req;
46             };
47              
48             sub process_response {
49 0     0     my ($elf, $resp) = @_;
50 0 0         warn "DigitalOcean >>>> ".$resp->as_string if $elf->{digitalocean_trace};
51              
52 0 0         if ($elf->{digitalocean_rate_limit_policy}) { # if this is turned on
53 0 0         if (my $limit = $resp->headers->header('RateLimit-Limit')) { # and if we actually got something
54 0           $elf->{digitalocean_rate_limit} = { Limit => $limit,
55             Remaining => $resp->headers->header('RateLimit-Remaining'),
56             Reset => $resp->headers->header('RateLimit-Reset'), };
57             }
58             }
59 0           $elf->SUPER::process_response( $resp );
60             }
61              
62             1;
63              
64             package Net::Async::DigitalOcean;
65              
66 12     12   5585 use strict;
  12         30  
  12         400  
67 12     12   90 use warnings;
  12         28  
  12         467  
68              
69 12     12   72 use JSON;
  12         32  
  12         158  
70 12     12   1927 use Data::Dumper;
  12         29  
  12         835  
71 12     12   92 use HTTP::Status qw(:constants);
  12         43  
  12         5540  
72              
73 12     12   8767 use Moose;
  12         5867969  
  12         100  
74              
75             our $VERSION = '0.04';
76              
77 12     12   113375 use Log::Log4perl qw(:easy);
  12         608221  
  12         84  
78             Log::Log4perl->easy_init($DEBUG);
79 12     12   9975 no warnings 'once';
  12         31  
  12         1539  
80             our $log = Log::Log4perl->get_logger("nado");
81              
82             =head1 NAME
83              
84             Net::Async::DigitalOcean - Async client for DigitalOcean REST APIv2
85              
86             =head1 SYNOPSIS
87              
88             use IO::Async::Loop;
89             my $loop = IO::Async::Loop->new; # the god-like event loop
90              
91             use Net::Async::DigitalOcean;
92             my $do = Net::Async::DigitalOcean->new( loop => $loop );
93             $do->start_actionables; # activate polling incomplete actions
94              
95             # create a domain, wait for it
96             $do->create_domain( {name => "example.com"} )
97             ->get; # block here
98              
99             # create a droplet, wait for it
100             my $dr = $do->create_droplet({
101             "name" => "www.example.com",
102             "region" => "nyc3",
103             "size" => "s-1vcpu-1gb",
104             "image" => "openfaas-18-04",
105             "ssh_keys" => [],
106             "backups" => 'true',
107             "ipv6" => 'true',
108             "monitoring" => 'true',
109             })
110             ->get; $dr = $dr->{droplet}; # skip type
111              
112             # reboot
113             $do->reboot(id => $dr->{id})->get;
114             # reboot all droplets tagged with 'prod:web'
115             $do->reboot(tag => 'prod:web')->get;
116              
117            
118              
119             =head1 OVERVIEW
120              
121             =head2 Platform
122              
123             L<DigitalOcean|https://www.digitalocean.com/> is a cloud provider which offers you to spin up
124             servers (droplets) with a specified OS, predefined sizes in predefined regions. You can also procure
125             storage volumes, attach those to the droplets, make snapshots of the volumes or the whole
126             droplet. There are also interfaces to create and manage domains and domain record, ssh keys, various
127             kinds of images or tags to tag the above things. On top of that you can build systems with load
128             balancers, firewalls, distributable objects (Spaces, similar to Amazon's S3). Or, you can go along
129             with the Docker pathway and/or create and run kubernetes structures.
130              
131             See the L<DigitalOcean Platform|https://docs.digitalocean.com/products/platform/> for more.
132              
133             DigitalOcean offers a web console to administrate all this, but also a
134             L<RESTy interface|https://docs.digitalocean.com/reference/api/>.
135              
136             =head2 REST API, asynchronous
137              
138             This client library can be used by applications to talk to the various DigitalOcean REST endpoints. But in contrast
139             to similar libraries, such as L<DigitalOcean> or L<WebService::DigitalOcean>, this library operates in I<asynchronous> mode:
140              
141             Firstly, all HTTP requests are launched asynchronously, without blocking until their respective responses come in.
142              
143             But more importantly, L<long-lasting actions|https://www.digitalocean.com/community/tutorials/how-to-use-and-understand-action-objects-and-the-digitalocean-api>,
144             such as creating a droplet, snapshoting volumes or rebooting a set of droplets are handled by the
145             library itself; the application does not need to keep track of these open actions, or keep polling
146             for their completion.
147              
148             The way this works is that the application first has to create the event loop and - with it -
149             create a handle to the DigitalOcean API server:
150              
151             use IO::Async::Loop;
152             my $loop = IO::Async::Loop->new;
153              
154             use Net::Async::DigitalOcean;
155             my $do = Net::Async::DigitalOcean->new( loop => $loop );
156             $do->start_actionables;
157              
158             You also should start a timer I<actionables>. In regular intervals it will check with the
159             server, whether open actions have been completed or not.
160              
161             With that, every method (except a few) return a L<Future> object, such when creating
162             a droplet:
163              
164             my $f = $do->create_droplet({
165             "name" => "example.com",
166             "region" => "nyc3",
167             "size" => "s-1vcpu-1gb",
168             "image" => "openfaas-18-04",
169             ....
170             });
171              
172             The application can either choose to wait synchronously:
173              
174             my $d = $f->get; # wait, and receive the response as HASH
175              
176             or, alternatively, can specify what should happen once the result comes in:
177              
178             $f->on_done( sub { my $d = shift;
179             warn "droplet $d->{droplet}->{name} ready (well, almost)"; } );
180              
181             Futures can also be combined in various ways; one extremely useful is to wait for several actions to
182             complete in one go:
183              
184             Future->wait_all(
185             map { $do->create_volume( ... ) }
186             qw(one two another) )->get;
187              
188             =head2 Success and Failure
189              
190             When futures succeed, the application will usually get a result in form of a Perl HASH (see below). If
191             a future fails and the failure is not handled specifically (by adding a C<< ->on_fail >> handler),
192             then an exception will be raised. The library tries to figure out what the real message from the
193             server was.
194              
195             =head2 Data Structures
196              
197             Another difference to other libraries in this arena is that it does not try to artifically
198             I<objectify> things into classes, such as for the I<droplet>, I<image> and other concepts.
199              
200             Instead, the library truthfully transports Perl HASHes and LISTs via JSON to the server and back;
201             even to the point to B<exactly> reflect the L<API specification|https://developers.digitalocean.com/documentation/v2/> .
202             That way you can always look up what to precisely expect as result.
203              
204             But as the server chooses to I<type> results, the application will have to cope with that
205              
206             my $d = $do->create_droplet({
207             "name" => "example.com",
208             ....
209             })->get;
210             $d = $d->{droplet}; # now I have the droplet itself
211              
212             =for readme include file="INSTALLATION" type="pod"
213              
214             =for readme stop
215              
216             =head2 Caveat Rate-Limiting
217              
218             To avoid being swamped the DigitalOcean server enforces several measures to limit abuse:
219              
220             =over
221              
222             =item * Limit on the number of HTTP requests within a certain time window.
223              
224             In the current version this client is rather aggressively trying to get things done. If you get
225             too many TOO_MANY_REQUESTS errors, you may want to increase the poll time of actions (see C<actionables>).
226              
227             Future version will support policies to be set by the application.
228              
229             =item * Limit on the total number of droplets to be created
230              
231             Such a case will result in an exception.
232              
233             =item * Limit on the number of droplets to be created in one go
234              
235             Such a case will result in an exception.
236              
237             =item * Limit in the number of snapshots
238              
239             In that case the client will wait for the indicated time. That may well be several minutes!
240              
241             =item * Limit in the size of volumes
242              
243             Such a case will result in an exception.
244              
245             =item * Limit in the size of droplets
246              
247             Such a case will result in an exception.
248              
249             =back
250              
251             =head1 INTERFACE
252              
253             There is only one object class here, that of the I<DigitalOcean> handle. All its methods - unless
254             specifically mentioned - typically return one L<Future> object.
255              
256             =head2 Constants
257              
258             =over
259              
260             =item * DIGITALOCEAN_API (string)
261              
262             Base HTTP endpoint for the DigitalOcean APIv2
263              
264             =back
265              
266             =cut
267              
268 12     12   172 use constant DIGITALOCEAN_API => 'https://api.digitalocean.com/v2';
  12         38  
  12         5926  
269              
270             =pod
271              
272             =head2 Constructor
273              
274             =cut
275              
276             has 'loop' => (isa => 'IO::Async::Loop', is => 'ro' );
277             has '_http' => (isa => 'Net::Async::HTTP', is => 'ro' );
278             has 'endpoint' => (isa => 'Str', is => 'ro' );
279             has '_actions' => (isa => 'HashRef', is => 'ro', default => sub { {} });
280             has '_actionables' => (isa => 'IO::Async::Timer::Periodic', is => 'rw' );
281             has 'rate_limit_frequency' => (isa => 'Int|Undef', is => 'ro', default => 2);
282             has 'bearer' => (isa => 'Str|Undef', is => 'ro' );
283              
284             =pod
285              
286             Following fields are honored:
287              
288             =over
289              
290             =item * C<loop> (required; L<IO::Async::Loop>)
291              
292             Event loop to keep things going.
293              
294              
295             =item * C<endpoint> (optional; string)
296              
297             If this field is completely omitted, then the DigitalOcean endpoint is chosen as default.
298              
299             If the field exists, but is kept C<undef>, then the environment variable C<DIGITALOCEAN_API> is
300             consulted. If that is missing, then an exception is raised.
301              
302             If the field exists, and the value is defined, it will be used.
303              
304             =item * C<bearer> (optional; string)
305              
306             To be authenticated to the official DigitalOcean endpoints the library will have to send
307             an C<Authentication> HTTP header with the I<bearer information> to the server. Once you
308             have an account, you can L<create such a bearer token|https://docs.digitalocean.com/reference/api/create-personal-access-token/>.
309              
310             If this C<bearer> field is missing or C<undef>, then the environment variable C<DIGITALOCEAN_BEARER>
311             will be consulted. If there is no such token, and the endpoint is the official one, an exception
312             will be raised. Otherwise, the missing bearer is tolerated (as you would if you test against a local
313             server).
314              
315             =item * C<throtteling> (optional; string)
316              
317             I<This is currently not implemented.>
318              
319             =item * C<tracing> (optional; any value)
320              
321             If set to something non-zero, then a HTTP trace (sending and receiving, headers and body) is written to
322             C<STDERR>. This helps tremendously during debugging.
323              
324             =item * C<rate_limit_frequency> (optional; integer; in seconds; default 5)
325              
326             This time interval is used to regularily poll the server for incomplete actions. Note, that for that
327             to happen, you have to start/stop the timer explicitly:
328              
329             $do->start_actionables; # from now on do something with DigitalOcean
330             $do->stop_actionables; # dont need it anymore
331              
332             =back
333              
334             =cut
335              
336             our $POLICY = {
337             SUPER_DEFENSIVE => {
338             '100%' => sub { 0; },
339             '70%' => sub { $_[0] + 1; },
340             '50%' => sub { $_[0] + 2; },
341             '30%' => sub { $_[0] + 10; },
342             }
343             };
344              
345             #
346              
347             around BUILDARGS => sub {
348             my $orig = shift;
349             my $class = shift;
350              
351             my %options = @_;
352              
353             $log->logdie ("IO::Async::Loop missing") unless $options{loop};
354              
355             my $endpoint = exists $options{endpoint} # if user hinted that ENV can be used
356             ? delete $options{endpoint} // $ENV{DIGITALOCEAN_API}
357             : DIGITALOCEAN_API;
358             $endpoint or $log->logdie ("no testing endpoint provided");
359              
360             my $bearer = delete $options{bearer} // $ENV{DIGITALOCEAN_BEARER}; # might be undef
361             $log->logdie ("bearer token missing") if ! defined $bearer && $endpoint eq DIGITALOCEAN_API;
362              
363             my $throtteling = delete $options{throtteling} // $ENV{DIGITALOCEAN_THROTTELING}; # might be undef
364             $throtteling = 1 if $endpoint eq DIGITALOCEAN_API; # no way around this
365              
366             my $tracing = delete $options{tracing}; # only via that path
367              
368 12     12   8698 use HTTP::Cookies;
  12         149526  
  12         2643  
369             my $http = Net::Async::DigitalOcean::RateLimited->new(
370             user_agent => "Net::Async::DigitalOcean $VERSION",
371             # timeout => 30,
372             cookie_jar => HTTP::Cookies->new(
373             file => "$ENV{'HOME'}/.digitalocean-perl-cookies",
374             autosave => 1, ),
375             );
376             $http->configure( +headers => { 'Authorization' => "Bearer $bearer" } ) if defined $bearer;
377             $http->{digitalocean_trace} = 1 if $tracing;
378             $http->{digitalocean_rate_limit_policy} = $POLICY->{SUPER_DEFENSIVE} if $throtteling;
379              
380             $options{loop}->add( $http );
381              
382             return $class->$orig (%options,
383             _http => $http,
384             endpoint => $endpoint,
385             bearer => $bearer,
386             );
387             };
388              
389             =pod
390              
391             =head2 Methods
392              
393             =head3 Polling the Server
394              
395             =over
396              
397             =item * C<start_actionables> ([ C<$interval> ])
398              
399             This starts the timer. The optional interval integer overrides what the C<$do> object would use as
400             default.
401              
402             =cut
403              
404             sub start_actionables {
405 0     0 1   my ($elf, $interval) = @_;
406              
407 0   0       $interval //= $elf->rate_limit_frequency;
408              
409 12     12   7536 use IO::Async::Timer::Periodic;
  12         12922  
  12         76699  
410             my $actionables = IO::Async::Timer::Periodic->new(
411             interval => $interval,
412             on_tick => sub {
413             #warn "tick";
414 0     0     my $actions = $elf->_actions; # handle
415              
416             # my %done; # collect done actions here
417 0           foreach my $action ( values %$actions ) {
418 0           my ($a, $f, $u, $r) = @$action;
419             # warn "looking at ".Dumper $a, $u, $r;
420 0 0         next if $a->{status} eq 'completed';
421 0 0         next unless defined $u; # virtual actions
422 0   0       $log->debug( "probing action $a->{id} for ".($a->{type}//$a->{rel}));
423             #warn "not completed asking for ".$a->{id}.' at '.$u;
424             # TODO type check
425 0           my $f2 = _mk_json_GET_future( $elf, $u );
426             $f2->on_done( sub {
427             #warn "action returned ".Dumper \@_;
428 0           my ($b) = @_; $b = $b->{action};
  0            
429             #warn "asking for action done, received ".Dumper $b;
430 0 0         if ($b->{status} eq 'completed') {
    0          
431             #warn "!!! completed with result $r".Dumper $r;
432 0 0         if ($f->is_done) { # this future has already been completed, THIS IS STRANGE
433 0           $log->warn("already completed action $a->{id} was again completed, ignoring...");
434             } else {
435 0           $action->[0] = $b; # replace the pending action with the completed version
436 0           $f->done( $r ); # if # report this as done, but ...
437             }
438             } elsif ($b->{status} eq 'errored') {
439 0           $f->fail( $b );
440             } # not completed: keep things as they are
441 0           } );
442             }
443             #warn "done ".Dumper [ keys %done ];
444             # delete $actions->{$_} for keys %done; # purge actions
445             },
446 0           );
447 0           $elf->_actionables( $actionables );
448 0           $elf->_http->loop->add( $actionables );
449 0           $actionables->start;
450             }
451              
452             =pod
453              
454             =item * C<stop_actionables>
455              
456             Simply stops the timer. At any time it can be restarted.
457              
458             =cut
459              
460             sub stop_actionables {
461 0     0 1   my ($elf) = @_;
462 0           $elf->_actionables->stop;
463             }
464              
465             =pod
466              
467             =back
468              
469             =cut
470            
471             #-- helper functions ---------------------------------------------------------------
472              
473             sub _mk_json_GET_futures {
474 0     0     my ($do, $path) = @_;
475              
476 0           $log->debug( "launching futures GET $path" );
477 0           my $f = $do->_http->loop->new_future;
478             #warn "futures setup ".$do->endpoint . $path;
479             $do->_http->GET( $do->endpoint . $path )
480             ->on_done( sub {
481 0     0     my ($resp) = @_;
482             #warn "futures resp ".Dumper $resp;
483 0 0         if ($resp->is_success) {
484 0 0         if ($resp->content_type eq 'application/json') {
485 0           my $data = from_json ($resp->content);
486 0 0 0       if ($data->{links} && (my $next = $data->{links}->{next})) { # we found a continuation
487             #warn "next $next";
488 0 0         $next =~ /page=(\d+)/ or $log->logdie ("cannot find next page inside '$next'");
489 0           my $page = $1;
490 0 0         if ( $path =~ /page=/ ) {
    0          
491 0           $path =~ s/page=\d+/page=$page/;
492             } elsif ($path =~ /\?/) {
493 0           $path .= "&page=$page"
494             } else {
495 0           $path .= "?page=$page";
496             }
497             #warn "pager $page path '$path'";
498 0           $f->done( $data, $do->_mk_json_GET_futures( $path ) );
499             } else {
500 0           $f->done( $data, undef );
501             }
502             } else {
503 0           $f->fail( "sizes not JSON" );
504             }
505             } else {
506 0           my $message = $resp->message; chop $message;
  0            
507 0           $f->fail( $message );
508             }
509             } )
510             ->on_fail( sub {
511 0     0     my ( $message ) = @_;
512 0           $log->logdie ("message from server '$message'");
513 0           } );
514 0           return $f;
515             }
516              
517             sub _mk_json_GET_future {
518 0     0     my ($do, $path) = @_;
519              
520 0           $log->debug( "launching future GET $path" );
521 0           my $f = $do->_http->loop->new_future;
522             $do->_http->GET( $do->endpoint . $path )
523             ->on_done( sub {
524 0     0     my ($resp) = @_;
525             #warn Dumper $resp;
526 0 0         if ($resp->is_success) {
527 0 0         if ($resp->content_type eq 'application/json') {
528 0           $f->done( from_json ($resp->content) );
529             } else {
530 0           $f->fail( "sizes not JSON" );
531             }
532             } else {
533 0           my $message= $resp->message; chop $message;
  0            
534 0           $f->fail( $message );
535             }
536             } )
537             ->on_fail( sub {
538 0     0     my ( $message ) = @_; chop $message;
  0            
539 0           $log->logdie ("message from server '$message'");
540 0           } );
541 0           return $f;
542             }
543              
544             sub _handle_response {
545 0     0     my ($do, $resp, $f) = @_;
546              
547             #warn "handle response ".Dumper $resp;
548             sub _message_crop {
549 0     0     my $message = $_[0]->message; chop $message;
  0            
550 0           return $message;
551             }
552              
553 0 0 0       if ($resp->code == HTTP_OK) {
    0          
    0          
    0          
    0          
    0          
554 0           $f->done( from_json ($resp->content) );
555              
556             } elsif ($resp->code == HTTP_NO_CONTENT) {
557 0           $f->done( );
558              
559             } elsif ($resp->code == HTTP_ACCEPTED
560             || $resp->code == HTTP_CREATED) { # for long-living actions
561             #warn "got accepted";
562 0 0         if (! $resp->content) { # yes, we can really get a ACCEPTED, but no content :/
    0          
563 0           $f->done( 42 );
564              
565             } elsif ($resp->content_type eq 'application/json') {
566 0           my $data = from_json ($resp->content);
567             #warn Dumper $data;
568 0 0         if (my $action = $data->{action}) { # if we only get an action to wait for
    0          
    0          
569             #warn "got action".Dumper $action;
570 0           $do->_actions->{ $action->{id} } = [ $action, $f, '/actions/'.$action->{id}, 42 ]; # memory this, the future, and a reasonable final result
571              
572             } elsif (my $links = $data->{links}) {
573             #warn "link actions";
574 0 0         if (my $res = $data->{droplet}) {
    0          
575 0           my $endpoint = $do->endpoint;
576 0           foreach my $action (@{ $links->{actions} }) { # should probably be only one entry
  0            
577             #warn "action found ".Dumper $action;
578 0           $action->{status} = 'in-progress'; # faking it
579 0           my $href = $action->{href};
580 0           $href =~ s/$endpoint//; # remove endpoint to make href relative
581 0           $do->_actions->{ $action->{id} } = [ $action, $f, $href, $res ]; # memory this, the future, and a reasonable final result
582             }
583              
584             } elsif ($res = $data->{droplets}) {
585             #warn "preliminary result".Dumper $res;
586 0           my @fs;
587             my @ids;
588             #warn "got actions";
589 0           foreach my $action (@{ $links->{actions} }) {
  0            
590             #warn "action found ".Dumper $action;
591 0           my $f2 = $do->_http->loop->new_future; # for every action we create a future
592 0           push @fs, $f2; # collect the futures
593 0           $action->{status} = 'in-progress'; # faking it
594 0           $do->_actions->{ $action->{id} } = [ $action, $f2, '/actions/'.$action->{id}, 42 ]; # memorize this, the future, the URL and a reasonable final result
595 0           push @ids, $action->{id}; # collect the ids
596             }
597             #warn "ids ".Dumper \@ids;
598             my $f3 = Future->wait_all( @fs ) # all these futures will be waited for to be done, before
599             ->then( sub { # warn "all subfutures done ";
600 0     0     $f->done( $res ); # the final future can be called done
601 0           } );
602 0           $do->_actions->{ join '|', @ids } = [ { id => 'xxx'.int(rand(10000)), # id does not matter
603             rel => 'compoud-create', # my invention
604             status => 'compound-in-progress' }, $f3, undef, $res ]; # compound, virtual action
605              
606             } else { # TODO, other stuff
607 0           warn "unhandled situation for ".Dumper $data;
608             }
609             } elsif (my $actions = $data->{actions}) { # multiple actions bundled (e.g. reboot several droplets)
610 0           my @fs;
611             my @ids;
612             #warn "got actions";
613 0           foreach my $action (@$actions) {
614             #warn "action found ".Dumper $action;
615 0           my $f2 = $do->_http->loop->new_future; # for every action we create a future
616 0           push @fs, $f2; # collect the futures
617 0           $do->_actions->{ $action->{id} } = [ $action, $f2, '/actions/'.$action->{id}, 42 ]; # memorize this, the future, the URL and a reasonable final result
618 0           push @ids, $action->{id}; # collect the ids
619             }
620             my $f3 = Future->wait_all( @fs ) # all these futures will be waited for to be done, before
621             ->then( sub { # warn "all subfutures done ";
622 0     0     $f->done( 42 ); # the final future can be called done
623 0           } );
624 0           $do->_actions->{ join '|', @ids } = [ { id => 'xxx', # id does not matter
625             status => 'compound-in-progress' }, $f3, undef, 42 ]; # compound, virtual action
626            
627             } else {
628 0           $f->done( $data );
629             # warn "not handled reaction from the server ".Dumper $data;
630             # $f->done( 42 );
631             }
632             } else {
633 0           $f->fail( "returned data not JSON" );
634             }
635             } elsif ($resp->is_redirect) {
636 0           $f->fail( _message_crop( $resp ) );
637              
638             } elsif ($resp->code == HTTP_TOO_MANY_REQUESTS) {
639 0           my $json = $resp->content;
640 0           my $data = from_json ($json);
641             #warn "message ".$data->{message};
642 0           my $bounce_time; # agenda
643 0 0         if ($data->{message} =~ /rate-limited.+?(\d+)m(\d+)s/) { # detect a hint that this operation is limited
644             #warn ">>>$1<<>>$2<<<";
645 0           $bounce_time = $1 * 60 + $2; # seconds
646 0   0       $bounce_time //= 30; # default
647             } else {
648 0           $bounce_time = 30; # just guessing something
649             }
650 0           $log->info( "server sent HTTP_TOO_MANY_REQUEST => will have to wait for $bounce_time seconds, and then repeat request" );
651              
652             $do->loop->watch_time( after => $bounce_time,
653             code => sub {
654 0     0     $log->debug( "repeating previously failed request to ".$resp->request->uri );
655             $do->_http->do_request( request => $resp->request )
656             ->on_done( sub {
657 0           my ($resp) = @_;
658 0           _handle_response( $do, $resp, $f );
659             } )
660             ->on_fail( sub {
661 0           my ( $message ) = @_; chop $message;
  0            
662 0           $log->logdie ("message from server '$message'");
663 0           } );
664 0           });
665              
666              
667             } elsif (! $resp->is_success) {
668             #warn "failed request ".$resp->message . ' (' . $resp->code . ') '. $resp->content;
669 0 0         if (my $json = $resp->content) {
670 0           my $data = from_json ($json);
671             #warn "error JSON ".Dumper $data;
672 0           $f->fail( $data->{message} );
673             } else {
674 0           $f->fail( _message_crop( $resp ));
675             }
676              
677             } else { # some other response
678 0           warn "unhandled request ".$resp->message . ' (' . $resp->code . ') '. $resp->content;
679 0           $f->fail( _message_crop( $resp ));
680             }
681             }
682              
683             sub _mk_json_POST_future {
684 0     0     my ($do, $path, $body) = @_;
685              
686 0           $log->debug( "launching future POST $path" );
687              
688 0           my $f = $do->_http->loop->new_future;
689             $do->_http->POST( $do->endpoint . $path,
690             to_json( $body),
691             content_type => 'application/json' )
692             ->on_done( sub {
693 0     0     my ($resp) = @_;
694             #warn "response ".Dumper $resp;
695 0           _handle_response( $do, $resp, $f );
696             } )
697             ->on_fail( sub {
698 0     0     my ( $message ) = @_; chop $message;
  0            
699             #warn "XXXXX $message";
700 0           $log->logdie ("message from server '$message'");
701 0           } );
702 0           return $f;
703             }
704              
705             sub _mk_json_PUT_future {
706 0     0     my ($do, $path, $body) = @_;
707              
708 0           $log->debug( "launching future PUT $path" );
709 0           my $f = $do->_http->loop->new_future;
710             $do->_http->PUT( $do->endpoint . $path,
711             to_json( $body),
712             content_type => 'application/json' )
713             ->on_done( sub {
714 0     0     my ($resp) = @_;
715             #warn "response ".Dumper $resp;
716 0           _handle_response( $do, $resp, $f );
717             } )
718             ->on_fail( sub {
719 0     0     my ( $message ) = @_; chop $message;
  0            
720 0           $log->logdie ("message from server '$message'");
721 0           } );
722 0           return $f;
723             }
724              
725             sub _mk_json_DELETE_future {
726 0     0     my ($do, $path, $headers) = @_;
727              
728 0           $log->debug( "launching future DELETE $path" );
729 0           my $f = $do->_http->loop->new_future;
730             $do->_http->do_request( uri => $do->endpoint . $path,
731             method => "DELETE",
732             ($headers ? (headers => $headers) : ()), )
733             ->on_done( sub {
734 0     0     my ($resp) = @_;
735             #warn Dumper $resp;
736 0           _handle_response( $do, $resp, $f );
737              
738             # if ($resp->code == HTTP_NO_CONTENT) {
739             # $f->done( );
740             # } elsif ($resp->code == HTTP_ACCEPTED) {
741             # $f->done( );
742             # } else {
743             # $f->fail( $resp->message );
744             # }
745             } )
746             ->on_fail( sub {
747 0     0     my ( $message ) = @_; chop $message;
  0            
748 0           $log->logdie ("message from server '$message'");
749 0 0         } );
750 0           return $f;
751             }
752              
753             =pod
754              
755             =head3 Meta Interface
756              
757             If you work with the official DigitalOcean server, then this section can/should be ignored.
758              
759             This subinterface allows to communicate with test servers to better control the test environent.
760              
761             =over
762              
763             =item * C<meta_reset>
764              
765             This deletes ALL resources on the server, providing a clean slate for a following test.
766              
767             =cut
768              
769             sub meta_reset {
770 0     0 1   my ($do) = @_;
771 0           return _mk_json_POST_future( $do, "meta/reset", {});
772             }
773              
774             =pod
775              
776             =item * C<meta_ping>
777              
778             This I<pings> the server which simply sends a I<pong> response.
779              
780             =cut
781              
782             sub meta_ping {
783 0     0 1   my ($do) = @_;
784 0           return _mk_json_POST_future( $do, "meta/ping", {});
785             }
786              
787             =pod
788              
789             =item * C<meta_account> (C<$account_HASH>)
790              
791             Typically sets/resets operational limits, such as the number of volumes or droplets to be created.
792             This will be more detailed later.
793              
794             =cut
795              
796             sub meta_account {
797 0     0 1   my ($do, $v) = @_;
798 0           return _mk_json_POST_future( $do, "meta/account", $v);
799             }
800              
801             =pod
802              
803             =item * C<meta_statistics>
804              
805             Returns eventually a rough statistics on what happened on the server.
806              
807             =cut
808              
809             sub meta_statistics {
810 0     0 1   my ($do) = @_;
811 0           return _mk_json_GET_future( $do, "meta/statistics");
812             }
813              
814             =pod
815              
816             =item * C<meta_capabilities>
817              
818             Lists which sections (chapters) of the L<API specification|https://developers.digitalocean.com/documentation/v2/>
819             are implemented on the server. Returns a HASH, to be detailed later.
820              
821             =cut
822              
823             sub meta_capabilities {
824 0     0 1   my ($do) = @_;
825 0           return _mk_json_GET_future( $do, "meta/capabilities");
826             }
827              
828             =pod
829              
830             =back
831              
832             =head3 L<Account|https://developers.digitalocean.com/documentation/v2/#account>
833              
834             =over
835              
836             =item * C<account>
837              
838             Returns account information for the current user (as identified by the I<bearer token>) as a HASH.
839              
840             =cut
841              
842             sub account {
843 0     0 1   my ($do) = @_;
844 0           return _mk_json_GET_future( $do, "/account" );
845             }
846              
847             =pod
848              
849             =back
850              
851             =head3 L<Block Storage|https://developers.digitalocean.com/documentation/v2/#list-all-block-storage-volumes>
852              
853             =over
854              
855             =item * C<volumes>
856              
857             List all volumes.
858              
859             =item * C<volumes> (name => C<$name>)
860              
861             List all volumes with a certain name.
862              
863             =cut
864              
865             sub volumes {
866 0     0 1   my ($do, $key, $val) = @_;
867            
868 0 0 0       if (defined $key && $key eq 'name') {
869 0           return _mk_json_GET_future( $do, "/volumes?name=$val" );
870             } else {
871 0           return _mk_json_GET_future( $do, '/volumes' );
872             }
873             }
874              
875             =pod
876              
877             =item * C<create_volume> (C<$volume_HASH>)
878              
879             Instigate to create a volume with your spec.
880              
881             =cut
882              
883             sub create_volume {
884 0     0 1   my ($do, $v) = @_;
885 0           return _mk_json_POST_future( $do, '/volumes', $v);
886             }
887            
888             =pod
889              
890             =item * C<volume> (id => C<$volume_id>)
891              
892             =item * C<volume> (name => C<$name>, C<$region>)
893              
894             Returns volume information, the volume either identified by its id, or the name/region combination.
895              
896             =cut
897              
898             sub volume {
899 0     0 1   my ($do, $key, $val, $reg) = @_;
900              
901 0 0         if ($key eq 'id') {
902 0           return _mk_json_GET_future( $do, "/volumes/$val" );
903             } else {
904 0           return _mk_json_GET_future( $do, "/volumes?name=$val&region=$reg" );
905             }
906             }
907              
908             =pod
909              
910             =item * C<snapshots> (volume => C<$volume_id>)
911              
912             List volume snapshots.
913              
914             =cut
915              
916             sub snapshots {
917 0     0 1   my ($do, $key, $val ) = @_;
918              
919 0 0         if ($key eq 'volume') {
    0          
920 0           return _mk_json_GET_future( $do, "/volumes/$val/snapshots");
921             } elsif ($key eq 'droplet') {
922 0           return _mk_json_GET_future( $do, "/droplets/$val/snapshots");
923             } else {
924 0           $log->logdie( "unhandled in method snapshots");
925             }
926             }
927              
928             =pod
929              
930             =item * C<create_snapshot> (C<$volume_id>, C<$HASH>)
931              
932             Creates a new volume snapshot with C<name> and C<tags> provided in the HASH.
933              
934             =cut
935              
936             sub create_snapshot {
937 0     0 1   my ($do, $volid, $s ) = @_;
938 0           return _mk_json_POST_future( $do, "/volumes/$volid/snapshots", $s);
939             }
940              
941             =pod
942              
943             =item * C<delete_volume> (id => C<$volume_id>)
944              
945             =item * C<delete_volume> (name => C<$name>, C<$region>)
946              
947             Delete a volume, either identified by its id, or the name/region combination.
948              
949             =cut
950              
951             sub delete_volume {
952 0     0 1   my ($do, $key, $val, $reg) = @_;
953              
954 0 0         if ($key eq 'id') {
    0          
955 0           return _mk_json_DELETE_future( $do, '/volumes/'. $val );
956              
957             } elsif ($key eq 'name') {
958 0           return _mk_json_DELETE_future( $do, "/volumes?name=$val&region=$reg" );
959              
960             } else {
961 0           $log->logdie ("invalid specification");
962             }
963             }
964            
965             =pod
966              
967             =item * C<delete_snapshot> (C<$snapshot_id>)
968              
969             Delete volume snapshot with a given id.
970              
971             =cut
972              
973             sub delete_snapshot {
974 0     0 1   my ($do, $id) = @_;
975 0           return _mk_json_DELETE_future( $do, '/snapshots/'. $id );
976             }
977              
978             =pod
979              
980             =back
981              
982             =head3 L<Block Storage Actions|https://developers.digitalocean.com/documentation/v2/#attach-a-block-storage-volume-to-a-droplet>
983              
984             =over
985              
986             =item * C<volume_attach> (C<$volume_id>, C<$attach_HASH>)
987              
988             Attaches a given volume to a droplet specified in the HASH.
989              
990             Attaching by name is NOT IMPLEMENTED.
991              
992             Note that the region of the droplet and that of the volume must agree to make that work.
993              
994             =item * C<volume_detach> (C<$volume_id>, C<$attach_HASH>)
995              
996             Detach the specified volume from the droplet named in the HASH.
997              
998             Detaching by name is NOT IMPLEMENTED.
999              
1000             =cut
1001              
1002             sub volume_attach {
1003 0     0 1   my ($do, $vid, $attach) = @_;
1004 0           return _mk_json_POST_future( $do, "/volumes/$vid/actions", $attach);
1005             }
1006              
1007             sub volume_detach {
1008 0     0 1   my ($do, $vid, $attach) = @_;
1009 0           return _mk_json_POST_future( $do, "/volumes/$vid/actions", $attach);
1010             }
1011              
1012             =pod
1013              
1014             =item * C<volume_resize> (C<$volume_id>, C<$resize_HASH>)
1015              
1016             Resizes the volume.
1017              
1018             =cut
1019              
1020             sub volume_resize {
1021 0     0 1   my ($do, $vid, $resize) = @_;
1022 0           return _mk_json_POST_future( $do, "/volumes/$vid/actions", $resize);
1023             }
1024              
1025             =pod
1026              
1027             =back
1028              
1029             =head3 L<Domains|https://developers.digitalocean.com/documentation/v2/#list-all-domains>
1030              
1031             =over
1032              
1033             =item * C<domains>
1034              
1035             Lists all domains.
1036              
1037             =cut
1038              
1039             sub domains {
1040 0     0 1   my ($do) = @_;
1041 0           return _mk_json_GET_futures( $do, "/domains" );
1042             }
1043              
1044             =pod
1045              
1046             =item * C<create_domain> (C<$domain_HASH>)
1047              
1048             Creates a domain entry with the given specification.
1049              
1050             Note that you can enter here anything, as the DigitialOcean DNS servers are not necessarily
1051             authoritative for such a domain.
1052              
1053             =cut
1054              
1055             sub create_domain {
1056 0     0 1   my ($do, $d) = @_;
1057 0           return _mk_json_POST_future( $do, '/domains', $d);
1058             }
1059              
1060             =pod
1061              
1062             =item * C<domain> (C<$name>)
1063              
1064             Retrieves information of a named domain.
1065              
1066             =cut
1067              
1068             sub domain {
1069 0     0 1   my ($do, $name) = @_;
1070 0           return _mk_json_GET_future( $do, "/domains/$name");
1071             }
1072              
1073             =pod
1074              
1075             =item * C<delete_domain> (C<$name>)
1076              
1077             Deletes the named domain.
1078              
1079             =cut
1080              
1081             sub delete_domain {
1082 0     0 1   my ($do, $name) = @_;
1083 0           return _mk_json_DELETE_future( $do, '/domains/'. $name );
1084             }
1085              
1086             =pod
1087              
1088             =back
1089              
1090             =head3 L<Domain Records|https://developers.digitalocean.com/documentation/v2/#list-all-domain-records>
1091              
1092             =over
1093              
1094             =item * C<domain_records>
1095              
1096             =item * C<domain_records> (C<$name>, type => C<$record_type>)
1097              
1098             =item * C<domain_records> (C<$name>, name => C<$record_name>)
1099              
1100             List domain records of the named domain; either all of them or filtered according to type or to name.
1101              
1102             =cut
1103              
1104             sub domain_records {
1105 0     0 1   my ($do, $name, %options) = @_;
1106              
1107 0           my @params;
1108             push @params, "type=$options{type}"
1109 0 0         if $options{type};
1110             push @params, "name=" . ($options{name} eq '@' ? $name : $options{name})
1111 0 0         if $options{name};
    0          
1112              
1113 0 0         return _mk_json_GET_futures( $do, "/domains/$name/records" .(@params ? '?'.join '&', @params : '') );
1114             }
1115              
1116             =pod
1117              
1118             =item * C<create_record> (C<$name>, C<$record_HASH>)
1119              
1120             Create new domain record within the named domain.
1121              
1122             =cut
1123              
1124             sub create_record {
1125 0     0 1   my ($do, $name, $r) = @_;
1126 0           return _mk_json_POST_future( $do, "/domains/$name/records", $r);
1127             }
1128              
1129             =pod
1130              
1131             =item * C<domain_record> (C<$name>, C<$record_id>)
1132              
1133             Retrieves the record for a given id from the named domain.
1134              
1135             =cut
1136              
1137             sub domain_record {
1138 0     0 1   my ($do, $name, $id) = @_;
1139 0           return _mk_json_GET_future( $do, "/domains/$name/records/$id");
1140             }
1141              
1142             =pod
1143              
1144             =item * C<update_record> (C<$name>, C<$record_id>, C<$record_HASH>)
1145              
1146             Selectively updates information in the record hash into the domain record with that id, all for the
1147             named domain.
1148              
1149              
1150             =cut
1151              
1152             sub update_record {
1153 0     0 1   my ($do, $name, $id, $r) = @_;
1154 0           return _mk_json_PUT_future( $do, "/domains/$name/records/$id", $r);
1155             }
1156              
1157             =pod
1158              
1159             =item * C<delete_record> (C<$name>, C<$record_id>)
1160              
1161             Deletes the record with the given id from the named domain.
1162              
1163             =cut
1164              
1165             sub delete_record {
1166 0     0 1   my ($do, $name, $id) = @_;
1167 0           return _mk_json_DELETE_future( $do, "/domains/$name/records/$id");
1168             }
1169              
1170             =pod
1171              
1172             =back
1173              
1174             =head3 L<Droplets|https://developers.digitalocean.com/documentation/v2/#create-a-new-droplet>
1175              
1176             =over
1177              
1178             =item * C<create_droplet> (C<$droplet_HASH>)
1179              
1180             Instigate to create new droplet(s) specified by the HASH.
1181              
1182             If you specify not a C<name> field, but a C<names> field with an ARRAY of names, then multiple
1183             droplets will be created. (There is a user-specific limit on how many can be created in one go.)
1184              
1185             Note that resulting droplets may have the networking information incomplete (as that seems
1186             to be determined rather late). To get this right, you will have to retrieve that droplet
1187             information a bit later.
1188              
1189             =cut
1190              
1191             sub create_droplet {
1192 0     0 1   my ($do, $v) = @_;
1193 0           return _mk_json_POST_future( $do, '/droplets', $v);
1194             }
1195              
1196             =pod
1197              
1198             =item * C<droplet> (id => C<$droplet_id>)
1199              
1200             =item * C<droplet> (name => C<$droplet_name>, C<$region>)
1201              
1202             Retrieve droplet information based on its id, or alternatively by name and region.
1203              
1204             =cut
1205              
1206             sub droplet {
1207 0     0 1   my ($do, $key, $val, $reg) = @_;
1208              
1209 0 0         if ($key eq 'id') {
1210 0           return _mk_json_GET_future( $do, "/droplets/$val" );
1211             } else {
1212 0           return _mk_json_GET_future( $do, "/droplets?name=$val&region=$reg" );
1213             }
1214             }
1215              
1216             =pod
1217              
1218             =item * C<droplets>
1219              
1220             List all droplets.
1221              
1222             Listing of droplets based on name is NOT IMPLEMENTED.
1223              
1224             =cut
1225              
1226             sub droplets {
1227 0     0 1   my ($do) = @_;
1228 0           return _mk_json_GET_futures( $do, "/droplets");
1229             }
1230              
1231             =pod
1232              
1233             =item * C<droplets_all>
1234              
1235             This B<convenience> method will return a future which - when done - will return the B<complete> list
1236             of droplets, not just the first page.
1237              
1238             =cut
1239              
1240 0           sub droplets_all {
1241 0     0 1   my ($do) = @_;
1242              
1243 0           my $g = $do->_http->loop->new_future;
1244 0           my @l = ();
1245              
1246 0           my $f = $do->droplets;
1247 0           _iprepare( $f, \@l, $g );
1248 0           return $g;
1249              
1250             sub _iprepare {
1251 0     0     my ($f, $l2, $g) = @_;
1252             $f->on_done( sub {
1253 0     0     (my $l, $f) = @_;
1254 0           push @$l2, @{ $l->{droplets} };
  0            
1255 0 0         if (defined $f) {
1256 0           _iprepare( $f, $l2, $g );
1257             } else {
1258 0           $g->done( { droplets => $l2, meta => { total => scalar @$l2 } } );
1259             }
1260 0           } );
1261             }
1262             }
1263              
1264             =pod
1265              
1266             =item * C<droplets_kernels>
1267              
1268             NOT IMPLEMENTED
1269              
1270             =item * C<snapshots> (droplet => C<$droplet_id>)
1271              
1272             List all droplet snapshots for that very droplet.
1273              
1274             =item * C<backups> (C<$droplet_id>)
1275              
1276             List backups of droplet specified by id.
1277              
1278             =cut
1279              
1280             sub backups {
1281 0     0 1   my ($do, $id ) = @_;
1282 0           return _mk_json_GET_future( $do, "/droplets/$id/backups");
1283             }
1284              
1285             =pod
1286              
1287             =item * C<droplet_actions> (id => C<$droplet_id>)
1288              
1289             =item * C<droplet_actions> (tag => C<$tag>)
1290              
1291             NOT IMPLEMENTED
1292              
1293             List all actions (also completed ones) of a specific droplet.
1294              
1295             =cut
1296              
1297             sub droplet_actions {
1298 0     0 1   my ($do, $key, $val) = @_;
1299              
1300 0 0         if ($key eq 'id') {
    0          
1301 0           return _mk_json_GET_future( $do, "/droplets/$val/actions" );
1302             } elsif ($key eq 'tag') {
1303 0           $log->logdie( "unhandled in method droplet_actions" );
1304             } else {
1305 0           $log->logdie( "unhandled in method droplet_actions" );
1306             }
1307             }
1308              
1309             =pod
1310              
1311             =item * C<delete_droplet> (id => C<$droplet_id>)
1312              
1313             =item * C<delete_droplet> (tag => C<$tag>)
1314              
1315             Delete a specific droplet by id, or alternatively, a set specified by a tag.
1316              
1317             =cut
1318              
1319             sub delete_droplet {
1320 0     0 1   my ($do, $key, $val) = @_;
1321              
1322 0 0         if ($key eq 'id') {
    0          
1323 0           return _mk_json_DELETE_future( $do, "/droplets/$val" );
1324             } elsif ($key eq 'tag') {
1325 0           return _mk_json_DELETE_future( $do, "/droplets?tag_name=$val" );
1326             } else {
1327 0           $log->logdie( "unhandled in method delete_droplet" );
1328             }
1329             }
1330            
1331             =pod
1332              
1333             =item * C<list_neighbors>
1334              
1335             NOT IMPLEMENTED
1336              
1337             =item * C<associated_resources> (id => C<$droplet_id>)
1338              
1339             List volumes attached, snapshots thereof, and snapshots of the droplet itself.
1340              
1341             =cut
1342              
1343             sub associated_resources {
1344 0     0 1   my ($do, $key, $val) = @_;
1345              
1346 0 0         if ($key eq 'id') {
    0          
1347 0           return _mk_json_GET_future( $do, "/droplets/$val/destroy_with_associated_resources" );
1348             } elsif ($key eq 'check_status') {
1349 0           return _mk_json_GET_future( $do, "/droplets/$val/destroy_with_associated_resources/status" );
1350             } else {
1351 0           $log->logdie( "unhandled in method associated_resources" );
1352             }
1353             }
1354            
1355             =pod
1356              
1357             =item * C<delete_selective_associated_resources>
1358              
1359             NOT IMPLEMENTED
1360              
1361             =item * C<delete_with_associated_resources> (id => C<$droplet_id>)
1362              
1363             Deletes the droplet and all its associated resources.
1364              
1365             =cut
1366              
1367             sub delete_with_associated_resources {
1368 0     0 1   my ($do, $key, $val) = @_;
1369              
1370 0 0         if ($key eq 'id') {
1371 0           return _mk_json_DELETE_future( $do, "/droplets/$val/destroy_with_associated_resources/dangerous", { 'X-Dangerous' => 'true' } );
1372             } else {
1373 0           $log->logdie( "unhandled in method delete_with_associated_resources" );
1374             }
1375             }
1376            
1377             =pod
1378              
1379             =item * C<associated_resources> (check_status => C<$droplet_id>)
1380              
1381             Check which resources are already deleted.
1382              
1383             =item * C<delete_with_associated_resources_retry>
1384              
1385             NOT IMPLEMENTED
1386              
1387             =back
1388              
1389             =head3 L<Droplet Actions|https://developers.digitalocean.com/documentation/v2/#droplet-actions>
1390              
1391             =over
1392              
1393             =item * C<enable_backups> (id => C<$droplet_id>)
1394              
1395             =item * C<enable_backups> (tag => C<$tag>)
1396              
1397             Enable regular backups (done by DigitalOcean).
1398              
1399             =cut
1400              
1401             sub enable_backups {
1402 0     0 1   my ($do, $key, $val) = @_;
1403 0           _perform_droplet_actions( $do, $key, $val, 'enable_backups' );
1404             }
1405              
1406             =pod
1407              
1408             =item * C<disable_backups> (id => C<$droplet_id>)
1409              
1410             =item * C<disable_backups> (tag => C<$tag>)
1411              
1412             Disable regular backups.
1413              
1414             =cut
1415              
1416             sub disable_backups {
1417 0     0 1   my ($do, $key, $val) = @_;
1418 0           _perform_droplet_actions( $do, $key, $val, 'disable_backups' );
1419             }
1420              
1421             =pod
1422              
1423             =item * C<reboot> (id => C<$droplet_id>)
1424              
1425             =item * C<reboot> (tag => C<$tag>)
1426              
1427             Reboots the specified droplet(s), either one via the id, or several via a tag.
1428              
1429             =cut
1430              
1431             sub reboot {
1432 0     0 1   my ($do, $key, $val) = @_;
1433 0           _perform_droplet_actions( $do, $key, $val, 'reboot' );
1434             }
1435              
1436             =pod
1437              
1438             =item * C<power_cycle> (id => C<$droplet_id>)
1439              
1440             =item * C<power_cycle> (tag => C<$tag>)
1441              
1442             Power-cycles the specified droplet(s), either one via the id, or several via a tag.
1443              
1444             =cut
1445              
1446             sub power_cycle {
1447 0     0 1   my ($do, $key, $val) = @_;
1448 0           _perform_droplet_actions( $do, $key, $val, 'power_cycle' );
1449             }
1450              
1451             =pod
1452              
1453             =item * C<shutdown> (id => C<$droplet_id>)
1454              
1455             =item * C<shutdown> (tag => C<$tag>)
1456              
1457             Shuts down the specified droplet(s), either one via the id, or several via a tag.
1458              
1459             =cut
1460              
1461             sub shutdown {
1462 0     0 1   my ($do, $key, $val) = @_;
1463 0           _perform_droplet_actions( $do, $key, $val, 'shutdown' );
1464             }
1465              
1466             =pod
1467              
1468             =item * C<power_off> (id => C<$droplet_id>)
1469              
1470             =item * C<power_off> (tag => C<$tag>)
1471              
1472             Powers down the specified droplet(s), either one via the id, or several via a tag.
1473              
1474             =cut
1475              
1476             sub power_off {
1477 0     0 1   my ($do, $key, $val) = @_;
1478 0           _perform_droplet_actions( $do, $key, $val, 'power_off' );
1479             }
1480              
1481             =pod
1482              
1483             =item * C<power_on> (id => C<$droplet_id>)
1484              
1485             =item * C<power_on> (tag => C<$tag>)
1486              
1487             Powers on the specified droplet(s), either one via the id, or several via a tag.
1488              
1489             =cut
1490              
1491             sub power_on {
1492 0     0 1   my ($do, $key, $val) = @_;
1493 0           _perform_droplet_actions( $do, $key, $val, 'power_on' );
1494             }
1495              
1496             =pod
1497              
1498             =item * C<restore> (id => C<$droplet_id>, C<$image>)
1499              
1500             =item * C<restore> (tag => C<$tag>, C<$image>)
1501              
1502             Restores the specified droplet(s) with the image given.
1503              
1504             =cut
1505              
1506             sub restore {
1507 0     0 1   my ($do, $key, $val, $image) = @_;
1508 0           _perform_droplet_action( $do, $key, $val, { type => 'restore', image => $image });
1509             }
1510              
1511             =pod
1512              
1513             =item * C<password_reset> (id => C<$droplet_id>)
1514              
1515             =item * C<password_reset> (tag => C<$tag>)
1516              
1517             Resets password on the specified droplet(s), either one via the id, or several via a tag.
1518              
1519             =cut
1520              
1521             sub password_reset {
1522 0     0 1   my ($do, $key, $val) = @_;
1523 0           _perform_droplet_actions( $do, $key, $val, 'password_reset' );
1524             }
1525              
1526             =pod
1527              
1528             =item * C<resize> (id => C<$droplet_id>, C<$new_size>, C<$diskresize_yes>)
1529              
1530             =item * C<resize> (tag => C<$tag>, C<$new_size>, C<$diskresize_yes>)
1531              
1532             Resizes the specified droplet(s).
1533              
1534             =cut
1535              
1536             sub resize {
1537 0     0 1   my ($do, $key, $val, $size, $disk) = @_;
1538 0           _perform_droplet_action( $do, $key, $val, { type => 'resize', size => $size, disk => $disk });
1539             }
1540              
1541             =pod
1542              
1543             =item * C<rebuild> (id => C<$droplet_id>, C<$image>)
1544              
1545             =item * C<rebuild> (tag => C<$tag>, C<$image>)
1546              
1547             Rebuilds the specified droplet(s) with the image given.
1548              
1549             NOTE: I do not understand the difference to C<restore>.
1550              
1551             =cut
1552              
1553             sub rebuild {
1554 0     0 1   my ($do, $key, $val, $image) = @_;
1555 0           _perform_droplet_action( $do, $key, $val, { type => 'rebuild', image => $image });
1556             }
1557              
1558             =pod
1559              
1560             =item * C<rename> (id => C<$droplet_id>, C<$name>)
1561              
1562             Renames the specified droplet to a new name.
1563              
1564             =cut
1565              
1566             sub rename {
1567 0     0 1   my ($do, $key, $val, $name) = @_;
1568 0           _perform_droplet_action( $do, $key, $val, { type => 'rename', name => $name });
1569             }
1570              
1571             =pod
1572              
1573             =item * C<enable_ipv6> (id => C<$droplet_id>)
1574              
1575             =item * C<enable_ipv6> (tag => C<$tag>)
1576              
1577             Turn on IPv6 on specified droplet(s).
1578              
1579             Note, that it takes a while on the server to get this configured.
1580              
1581             Note, that there does not seem a way to disable IPv6 for a droplet.
1582              
1583             =cut
1584              
1585             sub enable_ipv6 {
1586 0     0 1   my ($do, $key, $val) = @_;
1587 0           _perform_droplet_actions( $do, $key, $val, 'enable_ipv6' );
1588             }
1589              
1590             =pod
1591              
1592             =item * C<enable_private_networking> (id => C<$droplet_id>)
1593              
1594             =item * C<enable_private_networking> (tag => C<$tag>)
1595              
1596             Enables ... well.
1597              
1598             =cut
1599              
1600             sub enable_private_networking {
1601 0     0 1   my ($do, $key, $val) = @_;
1602 0           _perform_droplet_actions( $do, $key, $val, 'enable_private_networking' );
1603             }
1604              
1605             =pod
1606              
1607             =item * C<create_droplet_snapshot> (id => C<$droplet_id>)
1608              
1609             =item * C<create_droplet_snapshot> (tag => C<$tag>)
1610              
1611             Creates a new snapshot of the specified droplet(s).
1612              
1613             =cut
1614              
1615             sub create_droplet_snapshot {
1616 0     0 1   my ($do, $key, $val) = @_;
1617 0           _perform_droplet_actions( $do, $key, $val, 'snapshot');
1618             }
1619              
1620             =pod
1621              
1622             =item * C<droplet_action>
1623              
1624             NOT IMPLEMENTED
1625              
1626             =cut
1627              
1628              
1629             sub _perform_droplet_actions {
1630 0     0     my ($do, $key, $val, $type) = @_;
1631 0           _perform_droplet_action( $do, $key, $val, { type => $type });
1632             }
1633              
1634             sub _perform_droplet_action {
1635 0     0     my ($do, $key, $val, $body) = @_;
1636              
1637 0 0         if ($key eq 'id') {
    0          
1638 0           return _mk_json_POST_future( $do, "/droplets/$val/actions", $body );
1639             } elsif ($key eq 'tag') {
1640 0           return _mk_json_POST_future( $do, "/droplets/actions?tag_name=$val", $body );
1641             } else {
1642 0           $log->logdie( "unhandled in method _perform_droplet_action" );
1643             }
1644             }
1645              
1646             =pod
1647              
1648             =back
1649              
1650             =head3 L<Images|https://developers.digitalocean.com/documentation/v2/#images>
1651              
1652             =over
1653              
1654             =item * C<images>
1655              
1656             List all images.
1657              
1658             =item * C<images> (type => 'distribution')
1659              
1660             List all distribution images.
1661              
1662             =item * C<images> (type => 'application')
1663              
1664             List all application images.
1665              
1666             =item * C<images> (private => 'true')
1667              
1668             List all user images.
1669              
1670             =item * C<images> (tag_name => C<$tag>)
1671              
1672             List all images tagged with the tag.
1673              
1674             =cut
1675              
1676             sub images {
1677 0     0 1   my ($do, $key, $val) = @_;
1678 0 0         if ($key) {
1679 0           return _mk_json_GET_futures( $do, "/images?$key=$val");
1680             } else {
1681 0           return _mk_json_GET_futures( $do, "/images");
1682             }
1683             }
1684              
1685             =pod
1686              
1687             =item * C<images_all>
1688              
1689             This B<convenience> method returns a future, which - when done - will return complete list of
1690             images. For that it will iterate over all pages, if any, and collects all results into a list.
1691              
1692             =cut
1693              
1694 0           sub images_all {
1695 0     0 1   my $do = shift;
1696            
1697 0           my $g = $do->_http->loop->new_future; # the HTTP request to be finished eventually
1698 0           my @l = (); # into this list all results will be collected
1699              
1700 0           my $f = $do->images( @_ ); # launch the first request (with the original parameters)
1701 0           _prepare( $f, \@l, $g ); # setup the reaction to the incoming response
1702 0           return $g;
1703              
1704             sub _prepare {
1705 0     0     my ($f, $l2, $g) = @_;
1706             $f->on_done( sub { # when the response comes in
1707 0     0     (my $l, $f) = @_; # we get the result and (maybe) a followup future
1708 0           push @$l2, @{ $l->{images} }; # accumulate the result
  0            
1709 0 0         if (defined $f) { # if there is a followup
1710 0           _prepare( $f, $l2, $g ); # repeat and rinse
1711             } else {
1712 0           $g->done( $l2 ); # we are done set this as overall result
1713             }
1714 0           } );
1715             }
1716             }
1717              
1718             =pod
1719              
1720             =item * C<create_custom_image>
1721              
1722             NOT IMPLEMENTED
1723              
1724             =item * C<image>
1725              
1726             NOT IMPLEMENTED
1727              
1728             =item * C<update_image>
1729              
1730             NOT IMPLEMENTED
1731              
1732             =item * C<image_actions>
1733              
1734             NOT IMPLEMENTED
1735              
1736             =item * C<delete_image>
1737              
1738             NOT IMPLEMENTED
1739              
1740             =back
1741              
1742             =head3 L<Regions|https://developers.digitalocean.com/documentation/v2/#list-all-regions>
1743              
1744             =over
1745              
1746             =item * C<regions>
1747              
1748             List all available regions.
1749              
1750             =cut
1751              
1752             sub regions {
1753 0     0 1   my ($do) = @_;
1754 0           return _mk_json_GET_future( $do, "/regions" );
1755             }
1756              
1757             =pod
1758              
1759             =back
1760              
1761             =head3 L<Sizes|https://developers.digitalocean.com/documentation/v2/#list-all-sizes>.
1762              
1763             =over
1764              
1765             =item * C<sizes>
1766              
1767             List all sizes.
1768              
1769             =cut
1770              
1771             sub sizes {
1772 0     0 1   my ($do) = @_;
1773 0           return _mk_json_GET_future( $do, "/sizes" );
1774             }
1775              
1776             =pod
1777              
1778             =back
1779              
1780             =head3 L<SSH keys|https://developers.digitalocean.com/documentation/v2/#list-all-keys>
1781              
1782             =over
1783              
1784             =item * C<keys>
1785              
1786             List all keys.
1787              
1788             =cut
1789              
1790             sub keys {
1791 0     0 1   my ($do, $id) = @_;
1792 0           return _mk_json_GET_futures( $do, "/account/keys");
1793             }
1794              
1795             =pod
1796              
1797             =item * C<create_key> (C<$key_HASH>)
1798              
1799             Create a new key with a provided HASH.
1800              
1801             =cut
1802              
1803             sub create_key {
1804 0     0 1   my ($do, $key) = @_;
1805 0           return _mk_json_POST_future( $do, "/account/keys", $key);
1806             }
1807              
1808             =pod
1809              
1810             =item * C<key> (C<$key_id>)
1811              
1812             Retrieve existing key given by the id.
1813              
1814             =cut
1815              
1816             sub key {
1817 0     0 1   my ($do, $id) = @_;
1818 0           return _mk_json_GET_future( $do, "/account/keys/$id");
1819             }
1820              
1821             =pod
1822              
1823             =item * C<update_key> (C<$key_id>, C<$key_HASH>)
1824              
1825             Selectively update fields for a given key.
1826              
1827             =cut
1828              
1829             sub update_key {
1830 0     0 1   my ($do, $id, $key) = @_;
1831 0           return _mk_json_PUT_future( $do, "/account/keys/$id", $key);
1832             }
1833              
1834             =pod
1835              
1836             =item * C<delete_key> (C<$key_id>)
1837              
1838             Delete a specific key.
1839              
1840             =cut
1841              
1842             sub delete_key {
1843 0     0 1   my ($do, $id) = @_;
1844 0           return _mk_json_DELETE_future( $do, "/account/keys/$id");
1845             }
1846              
1847             =pod
1848              
1849             =back
1850              
1851             =head1 SEE ALSO
1852              
1853             =over
1854              
1855             =item * INSTALLATION file in this distribution
1856              
1857             =item * examples/*.pl in this distribution
1858              
1859             =item * t/*.t test suites in this distribution
1860              
1861             =item * L<Github|https://github.com/drrrho/net-async-digitalocean-perl>
1862              
1863             =item * Topic Map knowledge in ontologies/digitalocean-clients.atm in this distribution
1864              
1865             =item * L<DigitalOcean API|https://docs.digitalocean.com/reference/api/>
1866              
1867             =item * Other Perl packages which talk to DigitalOcean are L<DigitalOcean> and L<WebService::DigitalOcean>
1868              
1869             =back
1870              
1871             =head1 AUTHOR
1872              
1873             Robert Barta, C<< <rho at devc.at> >>
1874              
1875             =head1 LICENSE AND COPYRIGHT
1876              
1877             Copyright 2021 Robert Barta.
1878              
1879             This program is free software; you can redistribute it and/or modify it
1880             under the terms of the the Artistic License (2.0). You may obtain a
1881             copy of the full license at:
1882              
1883             L<http://www.perlfoundation.org/artistic_license_2_0>
1884              
1885             Any use, modification, and distribution of the Standard or Modified
1886             Versions is governed by this Artistic License. By using, modifying or
1887             distributing the Package, you accept this license. Do not use, modify,
1888             or distribute the Package, if you do not accept this license.
1889              
1890             If your Modified Version has been derived from a Modified Version made
1891             by someone other than you, you are nevertheless required to ensure that
1892             your Modified Version complies with the requirements of this license.
1893              
1894             This license does not grant you the right to use any trademark, service
1895             mark, tradename, or logo of the Copyright Holder.
1896              
1897             This license includes the non-exclusive, worldwide, free-of-charge
1898             patent license to make, have made, use, offer to sell, sell, import and
1899             otherwise transfer the Package with respect to any patent claims
1900             licensable by the Copyright Holder that are necessarily infringed by the
1901             Package. If you institute patent litigation (including a cross-claim or
1902             counterclaim) against any party alleging that the Package constitutes
1903             direct or contributory patent infringement, then this Artistic License
1904             to you shall terminate on the date that such litigation is filed.
1905              
1906             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
1907             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
1908             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
1909             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
1910             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
1911             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
1912             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
1913             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1914              
1915              
1916             =cut
1917              
1918              
1919             1;