Initial commit
[webservice-scaleway.git] / lib / WebService / Scaleway.pm
1 package WebService::Scaleway;
2
3 use 5.014000;
4 use strict;
5 use warnings;
6
7 our $VERSION = '0.001';
8
9 use Carp qw/croak/;
10 use HTTP::Tiny;
11 use JSON::MaybeXS;
12 use Scalar::Util qw/blessed/;
13
14 my $ht = HTTP::Tiny->new(
15 agent => "WebService-Scaleway/$VERSION ",
16 verify_SSL => 1,
17 );
18
19 # Instance of WebService::Scaleway with no API key
20 # Used to create tokens from email/password
21 my $dummy = '';
22 $dummy = bless \$dummy, __PACKAGE__;
23
24 sub _account ($) { "https://account.scaleway.com$_[0]"}
25 sub _api ($) { "https://api.scaleway.com$_[0]" }
26
27 sub _request {
28 my ($self, $method, $url, $opts) = @_;
29 $opts->{headers} //= {};
30 $opts->{headers}{'X-Auth-Token'} = $$self if $$self;
31 $opts->{headers}{'Content-Type'} = 'application/json';
32 my $ret = $ht->request($method, $url, $opts);
33 die 'Request to Scaleway API server was unsuccessful: ' . $ret->{status} . ' ' . $ret->{reason} . '; ' . $ret->{content} unless $ret->{success};
34
35 decode_json $ret->{content} if $ret->{status} != 204;
36 }
37
38 sub _get { shift->_request(GET => @_) }
39 sub _post { shift->_request(POST => @_) }
40 sub _patch { shift->_request(PATCH => @_) }
41 sub _put { shift->_request(PUT => @_) }
42 sub _delete { shift->_request(DELETE => @_) }
43
44 sub _tores {
45 my @ret = map { bless $_, 'WebService::Scaleway::Resource' } @_;
46 wantarray ? @ret : $ret[0]
47 }
48
49 sub new {
50 my ($class, $token) = @_;
51 $token = $dummy->create_token(@_[1..$#_])->id if @_ > 2;
52
53 bless \$token, $class
54 }
55
56 BEGIN {
57 my @account_res = qw/token organization user/;
58 my @api_res = qw/server volume snapshot image ip security_group/;
59
60 my %res = (
61 map ({ $_ => _account "/${_}s" } @account_res),
62 map { $_ => _api "/${_}s" } @api_res);
63
64 my %create_parms = (
65 token => [qw/email password expires/],
66 server => [qw/name organization image volumes tags/],
67 volume => [qw/name organization volume_type size/],
68 snapshot => [qw/name organization volume_id/],
69 image => [qw/name organization root_volume arch/],
70 ip => [qw/ organization/],
71 security_group => [qw/name organization description/],
72 );
73
74 sub dynsub {
75 no strict 'refs';
76 my $sub = pop;
77 *$_ = $sub for @_
78 }
79
80 for my $res (keys %res) {
81 dynsub $res, "get_$res", sub {
82 local *__ANON__ = $res;
83 _tores shift->_get("$res{$res}/$_[0]")->{$res}
84 };
85
86 dynsub $res.'s', "list_$res".'s', sub {
87 local *__ANON__ = $res.'s';
88 my @ret = _tores @{shift->_get($res{$res})->{$res.'s'}};
89 wantarray ? @ret : $ret[0]
90 };
91
92 dynsub "delete_$res", sub {
93 local *__ANON__ = "delete_$res";
94 shift->_delete("$res{$res}/$_[0]")
95 };
96
97 dynsub "create_$res", sub {
98 local *__ANON__ = "create_$res";
99 my $self = shift;
100 my $content = $_[0];
101 if (blessed $content || ref $content ne 'HASH') {
102 croak "create_$res does not understand positional parameters, pass a hashref instead\n" unless $create_parms{$res};
103 my @parms = @{$create_parms{$res}};
104 $content = { map {
105 $parms[$_] => (blessed $_[$_] ? $_[$_]->id : $_[$_]) } 0 .. $#_ };
106 }
107 _tores $self->_post($res{$res}, { content => encode_json $content })->{$res}
108 };
109
110 dynsub "update_$res", sub {
111 local *__ANON__ = "update_$res";
112 my $data = blessed $_[1] ? {%{$_[1]}} : $_[1];
113 shift->_put("$res{$res}/".$data->{id}, { content => encode_json $data })
114 };
115 }
116 }
117
118 sub security_group_rule {
119 _tores shift->_get(_api "/security_groups/$_[0]/rules/$_[1]")->{rule}
120 }
121
122 sub security_group_rules {
123 _tores @{shift->_get(_api "/security_groups/$_[0]/rules")->{rules}}
124 }
125
126 BEGIN {
127 *get_security_group_rule = \&security_group_rule;
128 *list_security_group_rule = \&security_group_rules;
129 }
130
131 sub delete_security_group_rule {
132 shift->_delete(_api "/security_groups/$_[0]/rules/$_[1]")
133 }
134
135 sub create_security_group_rule {
136 my $self = shift;
137 my $grp = shift;
138 my $content = $_[0];
139 unless (ref $content eq 'HASH') {
140 my @parms = qw/organization action direction ip_range protocol dest_port_from/;
141 $content = { map { $parms[$_] => $_[$_] } 0 .. $#_ };
142 }
143 $self->_post(_api "/security_groups/$grp/rules", { content => encode_json $content })
144 }
145
146 sub update_security_group_rule {
147 my $data = blessed $_[2] ? {%{$_[2]}} : $_[2];
148 shift->_put (_api "/security_groups/$_[0]/rules/".$data->{id}, { content => encode_json $data })
149 }
150
151 sub server_actions {
152 @{shift->_get(_api "/servers/$_[0]/action")->{actions}}
153 }
154
155 BEGIN { *list_server_actions = \&server_actions }
156
157 sub perform_server_action {
158 my $content = encode_json { action => $_[1] };
159 _tores shift->_post(_api "/servers/$_[0]/action", { content => $content })->{task};
160 }
161
162 sub refresh_token {
163 _tores shift->_patch(_account "/tokens/$_[0]")->{token}
164 }
165
166 sub server_metadata {
167 _tores $dummy->_get('http://169.254.42.42/conf?format=json')
168 }
169
170 package # hide from PAUSE
171 WebService::Scaleway::Resource;
172
173 use overload '""' => sub { shift->id };
174
175 our $AUTOLOAD;
176 sub AUTOLOAD {
177 my ($self) = @_;
178 my ($attr) = $AUTOLOAD =~ m/::([^:]*)$/s;
179 die "No such attribute: $attr" unless exists $self->{$attr};
180 $self->{$attr}
181 }
182
183 sub can {
184 my ($self, $sub) = @_;
185 exists $self->{$sub} ? sub { shift->{$sub} } : undef
186 }
187
188 sub DESTROY {} # Don't call AUTOLOAD on destruction
189
190 1;
191 __END__
192
193 =encoding utf-8
194
195 =head1 NAME
196
197 WebService::Scaleway - Perl interface to Scaleway cloud server provider API
198
199 =head1 SYNOPSIS
200
201 use WebService::Scaleway;
202 my $token = ...; # API token here
203 my $sw = WebService::Scaleway->new($token);
204 my $org = $sw->organizations;
205
206 # Create an IP, a volume, and use them for a new Debian Jessie server
207 my $ip = $sw->create_ip($org);
208 my $vol = $sw->create_volume('testvol', $org, 'l_ssd', 50_000_000_000);
209 my ($debian) = grep { $_->name =~ /debian jessie/i } $sw->images;
210 my $srv = $sw->create_server('testsrv', $org, $debian, {1 => {%$vol}});
211
212 # Change the server name
213 $srv->{name} = 'Debian';
214 $sw->update_server($srv);
215
216 # Boot the server
217 $sw->perform_server_action($srv, 'poweron');
218 say "The server is now booting. To access it, do ssh root@", $ip->address;
219
220 =head1 DESCRIPTION
221
222 Scaleway is an IaaS provider that offers bare metal ARM cloud servers.
223 WebService::Scaleway is a Perl interface to the Scaleway API.
224
225 =head2 Constructors
226
227 WebService::Scaleway objects are defined by their authentication
228 token. There are two consructors:
229
230 =over
231
232 =item WebService::Scaleway->B<new>(I<$auth_token>)
233
234 Construct a WebService::Scaleway object from a given authentication
235 token.
236
237 =item WebService::Scaleway->B<new>(I<$email>, I<$password>)
238
239 Construct a WebService::Scaleway object from an authentication token
240 obtained by logging in with the given credentials.
241
242 =back
243
244 =head2 Listing resources
245
246 These methods return a list of all resources of a given type
247 associated to your account. Each resource is a blessed hashref with
248 C<AUTOLOAD>ed accessors (for example C<< $resource->{name} >> can be
249 written as C<< $resource->name >>) and that stringifies to the ID of
250 the resource: C<< $resource->id >>.
251
252 There is no difference between B<resources>() and
253 B<list_resources>().
254
255 =over
256
257 =item $self->B<tokens>
258
259 =item $self->B<list_tokens>
260
261 Official documentation: L<https://developer.scaleway.com/#tokens-tokens-get>.
262
263 =item $self->B<organizations>
264
265 =item $self->B<list_organizations>
266
267 Official documentation: L<https://developer.scaleway.com/#organizations-organizations>.
268
269 =item $self->B<servers>
270
271 =item $self->B<list_servers>
272
273 Official documentation: L<https://developer.scaleway.com/#servers-servers-get>.
274
275 =item $self->B<volumes>
276
277 =item $self->B<list_volumes>
278
279 Official documentation: L<https://developer.scaleway.com/#volumes-volumes-get>.
280
281 =item $self->B<snapshots>
282
283 =item $self->B<list_snapshots>
284
285 Official documentation: L<https://developer.scaleway.com/#snapshots-snapshots-get>.
286
287 =item $self->B<images>
288
289 =item $self->B<list_images>
290
291 Official documentation: L<https://developer.scaleway.com/#images-images-get>.
292
293 =item $self->B<ips>
294
295 =item $self->B<list_ips>
296
297 Official documentation: L<https://developer.scaleway.com/#ips-ips-get>.
298
299 =item $self->B<security_groups>
300
301 =item $self->B<list_security_groups>
302
303 Official documentation: L<https://developer.scaleway.com/#security-groups-security-groups-get>.
304
305 =item $self->B<security_group_rules>(I<$group_id>)
306
307 =item $self->B<list_security_group_rules>(I<$group_id>)
308
309 Official documentation: L<https://developer.scaleway.com/#security-groups-manage-rules-get>.
310
311 =back
312
313 =head2 Retrieving resources
314
315 These methods take the id of a resource and return the resource as a
316 blessed hashref as described in the previous section.
317
318 There is no difference between B<resource>(I<$id>) and
319 B<get_resource>(I<$id>).
320
321 =over
322
323 =item $self->B<token>(I<$id>)
324
325 =item $self->B<get_token>(I<$id>)
326
327 Official documentation: L<https://developer.scaleway.com/#tokens-token-get>.
328
329 =item $self->B<user>(I<$id>)
330
331 =item $self->B<get_user>(I<$id>)
332
333 Official documentation: L<https://developer.scaleway.com/#users-user>.
334
335 =item $self->B<server>(I<$id>)
336
337 =item $self->B<get_server>(I<$id>)
338
339 Official documentation: L<https://developer.scaleway.com/#servers-server-get>.
340
341 =item $self->B<volume>(I<$id>)
342
343 =item $self->B<get_volume>(I<$id>)
344
345 Official documentation: L<https://developer.scaleway.com/#volumes-volume-get>.
346
347 =item $self->B<snapshot>(I<$id>)
348
349 =item $self->B<get_snapshot>(I<$id>)
350
351 Official documentation: L<https://developer.scaleway.com/#snapshots-snapshot-get>.
352
353 =item $self->B<image>(I<$id>)
354
355 =item $self->B<get_image>(I<$id>)
356
357 Official documentation: L<https://developer.scaleway.com/#images-operation-on-a-single-image-get>.
358
359 =item $self->B<ip>(I<$id>)
360
361 =item $self->B<get_ip>(I<$id>)
362
363 Official documentation: L<https://developer.scaleway.com/#ips-ip-get>.
364
365 =item $self->B<security_group>(I<$id>)
366
367 =item $self->B<get_security_group>(I<$id>)
368
369 Official documentation: L<https://developer.scaleway.com/#security-groups-operation-on-a-security-groups-get>.
370
371 =item $self->B<security_group_rule>(I<$group_id>, I<$rule_id>)
372
373 =item $self->B<get_security_group_rule>(I<$group_id>, I<$rule_id>)
374
375 Official documentation: L<https://developer.scaleway.com/#security-groups-operation-on-a-security-rule-get>.
376
377 =back
378
379 =head2 Deleting resources
380
381 These methods take the id of a resource and delete it. They do not
382 return anything.
383
384 =over
385
386 =item $self->B<delete_token>(I<$id>)
387
388 Official documentation: L<https://developer.scaleway.com/#tokens-token-delete>.
389
390 =item $self->B<delete_server>(I<$id>)
391
392 Official documentation: L<https://developer.scaleway.com/#servers-server-delete>.
393
394 =item $self->B<delete_volume>(I<$id>)
395
396 Official documentation: L<https://developer.scaleway.com/#volumes-volume-delete>.
397
398 =item $self->B<delete_snapshot>(I<$id>)
399
400 Official documentation: L<https://developer.scaleway.com/#snapshots-snapshot-delete>.
401
402 =item $self->B<delete_image>(I<$id>)
403
404 Official documentation: L<https://developer.scaleway.com/#images-operation-on-a-single-image-delete>.
405
406 =item $self->B<delete_ip>(I<$id>)
407
408 Official documentation: L<https://developer.scaleway.com/#ips-ip-delete>.
409
410 =item $self->B<delete_security_group>(I<$id>)
411
412 Official documentation: L<https://developer.scaleway.com/#security-groups-operation-on-a-security-groups-delete>.
413
414 =item $self->B<delete_security_group_rule>(I<$group_id>, I<$rule_id>)
415
416 Official documentation: L<https://developer.scaleway.com/#security-groups-operation-on-a-security-rule-delete>.
417
418 =back
419
420 =head2 Modifying resources
421
422 These methods take a hashref representing a resource that already
423 exists and update it. The value of C<< $resource->{id} >> is used for
424 identifying this resource on the remote end. Both blessed and
425 unblessed hashrefs are accepted. The updated resource is returned as a
426 blessed hashref as described in L</"Listing resources">.
427
428 =over
429
430 =item $self->B<update_server>(I<$resource>)
431
432 Official documentation: L<https://developer.scaleway.com/#servers-server-put>.
433
434 =item $self->B<update_snapshot>(I<$resource>)
435
436 Official documentation: L<https://developer.scaleway.com/#snapshots-snapshot-put>.
437
438 =item $self->B<update_image>(I<$resource>)
439
440 Official documentation: L<https://developer.scaleway.com/#images-operation-on-a-single-image-put>.
441
442 =item $self->B<update_ip>(I<$resource>)
443
444 Official documentation: L<https://developer.scaleway.com/#ips-ip-put>.
445
446 =item $self->B<update_security_group>(I<$resource>)
447
448 Official documentation: L<https://developer.scaleway.com/#security-groups-operation-on-a-security-groups-put>.
449
450 =item $self->B<update_security_group_rule>(I<$group_id>, I<$resource>)
451
452 Official documentation: L<https://developer.scaleway.com/#security-groups-operation-on-a-security-rule-put>.
453
454 =back
455
456 =head2 Creating resources
457
458 These methods take either a hash that is passed directly to the API or
459 a method-specific list of positional parameters. They create a new
460 resource and return it as a blessed hashref as described in
461 L</"Listing resources">.
462
463 When using positional parameters, you can pass a resource in blessed
464 hashref format where a resource ID is expected. The function will call
465 C<< ->id >> on the resouce automatically.
466
467 Most of these methods require an organization ID. You can obtain it
468 with the B<organizations> method described above.
469
470 =over
471
472 =item $self->B<create_token>(I<\%data>)
473
474 =item $self->B<create_token>(I<$email>, I<$password>, [I<$expires>])
475
476 Authenticates a user against their username and password and returns
477 an authentication token. If I<$expires> (default: false) is true, the
478 token will expire.
479
480 This method is called internally by the two-argument constructor.
481
482 Official documentation: L<https://developer.scaleway.com/#tokens-tokens-get>.
483
484 =item $self->B<create_server>(I<\%data>)
485
486 =item $self->B<create_server>(I<$name>, I<$organization>, I<$image>, I<$volumes>, [I<$tags>])
487
488 Creates and returns a new server.
489
490 I<$name> is the server name. I<$organization> is the organization ID.
491 I<$image> is the image ID. I<$volumes> is a "sparse array" (hashref
492 from indexes to volumes, indexed from 1) of volumes. I<$tags> is an
493 optional arrayref of tags.
494
495 Note that the volumes must be unblessed hashrefs. If I<$vol> is a
496 volume, you can use this idiom: C<< $volumes = {1 => {%$vol}} >>.
497
498 Official documentation: L<https://developer.scaleway.com/#servers-servers-get>.
499
500 =item $self->B<create_volume>(I<\%data>)
501
502 =item $self->B<create_volume>(I<$name>, I<$organization>, I<$volume_type>, I<$size>)
503
504 Creates and returns a new volume. I<$volume_type> currently must be
505 C<l_ssd>. I<$size> is the size in bytes.
506
507 Official documentation: L<https://developer.scaleway.com/#volumes-volumes-get>.
508
509 =item $self->B<create_snapshot>(I<\%data>)
510
511 =item $self->B<create_snapshot>(I<$name>, I<$organization>, I<$volume_id>)
512
513 Creates and returns a snapshot of the volume I<$volume_id>.
514
515 Official documentation: L<https://developer.scaleway.com/#snapshots-snapshots-get>.
516
517 =item $self->B<create_image>(I<\%data>)
518
519 =item $self->B<create_image>(I<$name>, I<$organization>, I<$root_volume>, I<$arch>)
520
521 Creates and returns an image from the volume I<$root_volume>. I<$arch>
522 is the architecture of the image (currently must be C<"arm">).
523
524 Official documentation: L<https://developer.scaleway.com/#images-images-get>.
525
526 =item $self->B<create_ip>(I<\%data>)
527
528 =item $self->B<create_ip>(I<$organization>)
529
530 Official documentation: L<https://developer.scaleway.com/#ips-ips-get>.
531
532 =item $self->B<create_security_group>(I<\%data>)
533
534 =item $self->B<create_security_group>(I<$name>, I<$organization>, I<$description>)
535
536 Official documentation: L<https://developer.scaleway.com/#security-groups-security-groups-get>.
537
538 =item $self->B<create_security_group_rule>(I<$group_id>)
539
540 =item $self->B<create_security_group_rule>(I<$group_id>, I<$organization>, I<$action>, I<$direction>, I<$ip_range>, I<$protocol>, [<$dest_port_from>])
541
542
543 Official documentation: L<https://developer.scaleway.com/#security-groups-manage-rules-get>.
544
545 =back
546
547 =head2 Miscellaneous methods
548
549 These are methods that don't fit any previous category. Any use of
550 "blessed hashref" refers to the concept described in L</"Listing
551 resources">. Wherever a resource ID is expected, you can instead pass
552 a resource as a blessed hashref and the method will call C<< ->id >>
553 on it for you.
554
555 =over
556
557 =item $self->B<server_actions>(I<$server_id>)
558
559 =item $self->B<list_server_actions>(I<$server_id>)
560
561 Returns a list of strings representing possible actions you can
562 perform on the given server. Example actions are powering on/off a
563 server or rebooting it.
564
565 Official documentation: L<https://developer.scaleway.com/#servers-actions-get>
566
567 =item $self->B<perform_server_action>(I<$server_id>, I<$action>)
568
569 Performs an action on a server. I<$action> is one of the strings
570 returned by B<server_actions>. The function returns a blessed hashref
571 with information about the task.
572
573 This is not very useful, as this module does not currently offer any
574 function for tracking tasks.
575
576 Official documentation: L<https://developer.scaleway.com/#servers-actions-post>
577
578 =item $self->B<refresh_token>(I<$token_id>)
579
580 This method takes the ID of an expirable token, extends its expiration
581 date by 30 minutes, and returns the new token as a blessed hashref.
582
583 Official documentation: L<https://developer.scaleway.com/#tokens-token-patch>
584
585 =item $self->B<server_metadata>
586
587 This method can only be called from a Scaleway server. It returns
588 information about the server as a blessed hashref.
589
590 Official documentation: L<https://developer.scaleway.com/#metadata-c1-server-metadata>
591
592 =back
593
594 =head1 SEE ALSO
595
596 L<https://developer.scaleway.com/>
597
598 =head1 AUTHOR
599
600 Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
601
602 =head1 COPYRIGHT AND LICENSE
603
604 Copyright (C) 2015 by Marius Gavrilescu
605
606 This library is free software; you can redistribute it and/or modify
607 it under the same terms as Perl itself, either Perl version 5.20.2 or,
608 at your option, any later version of Perl 5 you may have available.
609
610
611 =cut
This page took 0.055955 seconds and 4 git commands to generate.