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