1 /+
2 
3     Digital Ocean API
4     2014 - 2020 by Laeeth Isharc, Kaleidic Associates Advisory Limited, and Symmetry Investments
5 
6     ---
7         import symmetry.api.digitalocean;
8         import std.json;
9         import std.stdio;
10 
11 
12         void main(string[] args)
13         {
14             auto ocean=OceanAPI(OceanAPIKey);
15             auto result=Droplet.create( ocean,
16                                         "newemail.symmetry.ssociates.com",
17                                         OceanRegion.lon1,
18                                         "1Gb",
19                                         OceanImageId("debian-8-x64"),
20                                         ["ab:21:7e:22:e5:4c:95:23:e9:aa:f8:59:be:5f:96:24"]);
21             writefln(result.prettyPrint);
22             auto actions=ocean.listDroplets;
23             writefln(actions.prettyPrint);
24         /*    auto droplet=ocean.findDroplet("hoelderlin.symmetry.ssociates.com").result.retrieve;
25             writefln(droplet.prettyPrint);
26             auto keys=ocean.listKeys;
27             writefln("%s",keys); */
28         }
29 
30     ---
31 
32     really not tested
33     so far: reasonable results for
34     listDomains
35     listDroplets
36     listSizes
37     listKeys
38     listImages
39     findDroplet
40     Droplet.retrieve
41 +/
42 
43 ///
44 module symmetry.api.digitalocean;
45 import std.stdio;
46 import std.json;
47 import std.net.curl;
48 import std.exception : enforce,assumeUnique;
49 import std.conv:to;
50 import std.algorithm:countUntil,map,each;
51 import std.traits:EnumMembers;
52 import std.array:array,appender;
53 import std.format:format;
54 import std.variant:Algebraic;
55 
56 /**
57     Implemented in the D Programming Language 2015 by Laeeth Isharc and Kaleidic Associates
58     Boost Licensed
59     Use at your own risk - this is not tested at all and if you end up deleting all your
60     instances and creating 10,000 pricey new ones then it will not be my fault
61 */
62 
63 ///
64 shared static this()
65 {
66     OceanRegions=[EnumMembers!OceanRegion].map!(a=>a.toString).array.assumeUnique;
67     DropletActions=[EnumMembers!DropletAction].map!(a=>a.toString).array.assumeUnique;
68     OceanImages=
69     [
70         OceanGlobalImage(false,"CoreOS-766.4.0-(beta)",13578467,OceanDistro.CoreOS,""),
71         OceanGlobalImage(false,"CentOS-5.10-x64",6372321,OceanDistro.CentOS,""),
72         OceanGlobalImage(false,"5.10 x32",6372425, OceanDistro.CentOS),
73         OceanGlobalImage(false,"6.0 x64",6372581, OceanDistro.Debian),
74         OceanGlobalImage(false,"6.0 x32",6372662, OceanDistro.Debian),
75         OceanGlobalImage(false,"21 x64",9640922, OceanDistro.Fedora),
76         OceanGlobalImage(false,"10.1",10144573, OceanDistro.FreeBSD),
77         OceanGlobalImage(false,"12.04.5 x64",10321756, OceanDistro.Ubuntu),
78         OceanGlobalImage(false,"12.04.5 x32",10321777, OceanDistro.Ubuntu),
79         OceanGlobalImage(false,"7.0 x64",10322059, OceanDistro.Debian),
80         OceanGlobalImage(false,"7.0 x32",10322378, OceanDistro.Debian),
81         OceanGlobalImage(false,"7 x64",10322623, OceanDistro.CentOS),
82         OceanGlobalImage(false,"22 x64",12065782, OceanDistro.Fedora),
83         OceanGlobalImage(false,"15.04 x64",12658446, OceanDistro.Ubuntu),
84         OceanGlobalImage(false,"15.04 x32",12660649, OceanDistro.Ubuntu),
85         OceanGlobalImage(false,"8.1 x64",12778278, OceanDistro.Debian),
86         OceanGlobalImage(false,"8.1 x32",12778337, OceanDistro.Debian),
87         OceanGlobalImage(false,"14.04 x64",13089493, OceanDistro.Ubuntu),
88         OceanGlobalImage(false,"14.04 x32",13089823, OceanDistro.Ubuntu),
89         OceanGlobalImage(false,"6.7 x64",13090046, OceanDistro.CentOS),
90         OceanGlobalImage(false,"6.7 x32",13090097, OceanDistro.CentOS),
91         OceanGlobalImage(false,"10.2",13321858, OceanDistro.FreeBSD),
92         OceanGlobalImage(false,"815.0.0 (alpha)",13683512, OceanDistro.CoreOS),
93         OceanGlobalImage(false,"766.4.0 (stable)",13750582, OceanDistro.CoreOS),
94         OceanGlobalImage(false,"FreeBSD AMP on 10.1",10163059, OceanDistro.FreeBSD),
95         OceanGlobalImage(true,"Drone on 14.04",11774848, OceanDistro.Ubuntu,"Drone"),
96         OceanGlobalImage(true,"Cassandra on 14.04",12540744, OceanDistro.Ubuntu,"Cassandra"),
97         OceanGlobalImage(true,"ELK Logging Stack on 14.04",12542038, OceanDistro.Ubuntu,"ELK Logging"),
98         OceanGlobalImage(true,"Django on 14.04",12740667, OceanDistro.Ubuntu,"Django"),
99         OceanGlobalImage(true,"Mumble Server (murmur) on 14.04",12914152, OceanDistro.Ubuntu,"murmur"),
100         OceanGlobalImage(true,"Joomla! 3.4.3 on 14.04",13014869, OceanDistro.Ubuntu,"Joomla"),
101         OceanGlobalImage(true,"Magento-1.9.2.1 CE on 14.04",13115155, OceanDistro.Ubuntu,"Magento"),
102         OceanGlobalImage(true,"MongoDB 3.0.5 on 14.04",13115659, OceanDistro.Ubuntu,"MongoDB"),
103         OceanGlobalImage(true,"LEMP on 14.04",13138234, OceanDistro.Ubuntu,"LEMP"),
104         OceanGlobalImage(true,"LAMP on 14.04",13138235, OceanDistro.Ubuntu,"LAMP"),
105         OceanGlobalImage(true,"MediaWiki on 14.04",13185409, OceanDistro.Ubuntu,"MediaWiki"),
106         OceanGlobalImage(true,"WordPress on 14.04",13229890, OceanDistro.Ubuntu,"WordPress"),
107         OceanGlobalImage(true,"Ruby on Rails on 14.04 (Postgres, Nginx, Unicorn)",13400199, OceanDistro.Ubuntu,"Ruby on Rails"),
108         OceanGlobalImage(true,"MEAN on 14.04",13413549, OceanDistro.Ubuntu,"MEAN"),
109         OceanGlobalImage(true,"Drupal 7.39 on 14.04",13414327, OceanDistro.Ubuntu,"Drupal"),
110         OceanGlobalImage(true,"Docker 1.8.2 on 14.04",13495049, OceanDistro.Ubuntu,"Docker"),
111         OceanGlobalImage(true,"node v4.1.0 on 14.04",13586846, OceanDistro.Ubuntu,"nodejs"),
112         OceanGlobalImage(true,"Redis 3.0.4 on 14.04",13601457, OceanDistro.Ubuntu,"Redis"),
113         OceanGlobalImage(true,"ownCloud 8.1.3 on 14.04",13603669, OceanDistro.Ubuntu,"ownCloud"),
114         OceanGlobalImage(true,"GitLab 8.0.1 CE on 14.04",13670476, OceanDistro.Ubuntu,"GitLab"),
115         OceanGlobalImage(true,"Ghost 0.7.1 on 14.04",13750431, OceanDistro.Ubuntu,"Ghost"),
116         OceanGlobalImage(true,"Dokku v0.4.1 on 14.04",13750783, OceanDistro.Ubuntu,"Dokku"),
117         OceanGlobalImage(true,"Discourse on 14.04",13846109, OceanDistro.Ubuntu,"Discourse"),
118         OceanGlobalImage(true,"PHPMyAdmin on 14.04",11730661, OceanDistro.Ubuntu,"PHPMyAdmin"),
119         OceanGlobalImage(true,"Redmine on 14.04",12438838, OceanDistro.Ubuntu,"Redmine"),
120     ];
121 }
122 
123 ///
124 string joinUrl(string url, string endpoint)
125 {
126     enforce(url.length>0, "broken url");
127     if (url[$-1]=='/')
128         url=url[0..$-1];
129     return url~"/"~endpoint;
130 }
131 
132 
133 ///
134 struct OceanAPI
135 {
136     string endpoint = "https://api.digitalocean.com/v2/";
137     string token;
138 
139     this(string token)
140     {
141         this.token=token;
142     }
143     this(string endpoint, string token)
144     {
145         this.endpoint=endpoint;
146         this.token=token;
147     }
148 }
149 
150 ///
151 JSONValue request(OceanAPI api, string url, HTTP.Method method=HTTP.Method.get, JSONValue params=JSONValue(null))
152 {
153     enforce(api.token.length>0,"no token provided");
154     url=api.endpoint.joinUrl(url);
155     auto client=HTTP(url);
156     client.addRequestHeader("Authorization", "Bearer "~api.token);
157     auto response=appender!(ubyte[]);
158     client.method=method;
159     switch(method) with(HTTP.Method)
160     {
161         case del:
162             client.setPostData(cast(void[])params.toString,"application/x-www-form-urlencoded");
163             break;
164         case get,head:
165             client.setPostData(cast(void[])params.toString,"application/json");
166             break;
167         default:
168             client.setPostData(cast(void[])params.toString,"application/json");
169             break;
170     }
171     client.onReceive = (ubyte[] data)
172     {
173         response.put(data);
174         return data.length;
175     };
176     client.perform();                 // rely on curl to throw exceptions on 204, >=500
177     return parseJSON(cast(string)response.data);
178 }
179 
180 
181 /// List all Actions
182 auto listActions(OceanAPI api)
183 {
184     return api.request("actions",HTTP.Method.get);
185 }
186 
187 /// retrieve existing Action
188 auto retrieveAction(OceanAPI api, string id)
189 {
190     return api.request("actions/"~id, HTTP.Method.get);
191 }
192 
193 ///
194 auto allNeighbours(OceanAPI api)
195 {
196     return api.request("reports/droplet_neighbors",HTTP.Method.get);
197 }
198 
199 ///
200 auto listUpgrades(OceanAPI api)
201 {
202     return api.request("droplet_upgrades",HTTP.Method.get);    
203 }
204 
205 /// List all Domains (managed through Ocean DNS interface)
206 auto listDomains(OceanAPI api)
207 {
208     return api.request("domains", HTTP.Method.get);
209 }
210 
211 ///
212 struct OceanDomain
213 {
214     OceanAPI api;
215     string id;
216     alias id this;
217     this(OceanAPI api, string id)
218     {
219         this.api=api;
220         this.id=id;
221     }
222     // Create new Domain
223     static auto create(OceanAPI api, string name, string ip)
224     {
225         JSONValue params;
226         params["name"]=name;
227         params["ip_address"]=ip;
228         return api.request("domains", HTTP.Method.post, params);
229     }
230     auto request(string url, HTTP.Method method=HTTP.Method.get, JSONValue params=JSONValue(null))
231     {
232         return api.request(url,method,params);
233     }
234 }
235 
236 /// Retrieve an existing Domain
237 auto get(OceanDomain domain)
238 {
239     return domain.request("domains/"~domain.id, HTTP.Method.get);
240 }
241 
242 /// Delete a Domain
243 auto del(OceanDomain domain)
244 {
245     return domain.request("domains/"~domain.id, HTTP.Method.del);
246 }
247 
248 ///  List all Domain Records
249 auto listDomainRecords(OceanDomain domain)
250 {
251     return domain.request(format("domains/%s/records",domain.id), HTTP.Method.get);
252 }
253 
254 /// Create a new Domain Record
255 auto createRecord(OceanDomain domain, string rtype=null, string name=null, string data=null,
256                          string priority=null, string port=null, string weight=null)
257 {
258     JSONValue params;
259     params["type"]=rtype;
260     if(name.length>0)
261         params["name"]=name;
262     if(data.length>0)
263         params["data"]=data;
264     if(priority.length>0)
265         params["priority"]=priority;
266     if(port.length>0)
267         params["port"]=port;
268     if(weight.length>0)
269         params["weight"]=weight;
270     return domain.request(
271         format("domains/%s/records",domain.id), HTTP.Method.post, params);
272 }
273 
274 ///  Retrieve an existing Domain Record
275 auto getRecord(OceanDomain domain, string recordId)
276 {
277     return domain.request(
278         format("domains/%s/records/%s",domain.id,recordId), HTTP.Method.get);
279 }
280 
281 ///  Delete a Domain Record
282 auto delRecord(OceanDomain domain, string recordId)
283 {
284     return domain.request(
285         format("domains/%s/records/%s",domain.id,recordId), HTTP.Method.del);
286 }
287 
288 ///  Update a Domain Record
289 auto updateRecord(OceanDomain domain, string recordId,string name)
290 {
291     JSONValue params;
292     params["name"] = name;
293     return domain.request(format("domains/%s/records/%s",domain.id, recordId), HTTP.Method.put, params);
294 }
295 
296 
297 /// list all droplets
298 auto listDroplets(OceanAPI api)
299 {
300     return api.request("droplets",HTTP.Method.get);
301 }
302 
303 ///
304 enum OceanRegion
305 {
306     ams2,
307     ams3,
308     fra1,
309     lon1,
310     nyc1,
311     nyc2,
312     nyc3,
313     sfo1,
314     sgp1,
315     tor1,
316 }
317 
318 
319 ///
320 immutable string[] OceanRegions;
321 ///
322 OceanRegion oceanRegion(string region)
323 {
324     OceanRegion ret;
325     auto i=OceanRegions.countUntil(region);
326     enforce(i>=0, new Exception("unknown droplet region: "~region));
327     return cast(OceanRegion)i;
328 }
329 
330 ///
331 string toString(OceanRegion region)
332 {
333     final switch(region) with(OceanRegion)
334     {
335         case ams2:
336             return "Amsterdam 2";
337         case ams3:
338             return "Amsterdam 3";
339         case fra1:
340             return "Frankfurt 1";
341         case lon1:
342             return "London 1";
343         case nyc1:
344             return "New York 1";
345         case nyc2:
346             return "New York 2";
347         case nyc3:
348             return "New York 3";
349         case sfo1:
350             return "San Francisco 1";
351         case sgp1:
352             return "Singapore 1";
353         case tor1:
354             return "Toronto 1";
355     }
356     assert(0);
357 }
358 
359 ///
360 enum OceanDistro
361 {
362     CoreOS,
363     Debian,
364     Fedora,
365     CentOS,
366     FreeBSD,
367     Ubuntu,
368 }
369 ///
370 alias OceanImageId=Algebraic!(int,string);
371 ///
372 struct OceanGlobalImage
373 {
374     bool isApplication=false;
375     string slug;
376     int id;
377     OceanDistro distro;
378     string application;
379 }
380 ///
381 OceanGlobalImage[] OceanImages;
382 
383 
384 ///
385 struct Droplet
386 {
387     OceanAPI api;
388     int id;
389 
390     this(OceanAPI api, int id)
391     {
392         this.api=api;
393         this.id=id;
394     }
395 
396     string toString()
397     {
398         return id.to!string;
399     }
400     auto request(string uri, HTTP.Method method=HTTP.Method.get, JSONValue params=JSONValue(null))
401     {
402         return api.request(uri,method,params);
403     }
404 
405     //  Create a new Droplet
406     static auto create(OceanAPI api,string name, OceanRegion region, string size, OceanImageId image, string[] sshKeys, string backups=null,
407                string ipv6=null, string privateNetworking=null, string userData=null)
408     {
409         JSONValue params;
410         params["name"]=name;
411         params["region"]=region.to!string;
412         params["size"]=size;
413         if(image.type==typeid(string))
414             params["image"]=image.get!string;
415         else
416             params["image"]=image.get!int;
417         if (sshKeys.length>0)
418             params["ssh_keys"]=sshKeys;
419         if(backups.length>0)
420             params["backups"]=backups.to!bool;
421         if (ipv6.length>0)
422             params["ipv6"]=ipv6.to!bool;
423         if (privateNetworking.length>0)
424             params["private_networking"]=privateNetworking.to!bool;
425         if (userData.length>0)
426             params["user_data"]=userData;
427         return api.request("droplets", HTTP.Method.post, params);
428     }
429 }
430 
431 ///  Makes an action
432 JSONValue action(Droplet droplet, DropletAction actionType,JSONValue params=JSONValue(null))
433 {
434     params["type"]=actionType.toString;
435     return droplet.request(format("droplets/%s/actions",droplet.id), HTTP.Method.post, params);
436 }
437 
438 ///
439 enum DropletAction
440 {
441     reboot,
442     powerCycle,
443     shutdown,
444     powerOff,
445     powerOn,
446     passwordReset,
447     resize,
448     restore,
449     rebuild,
450     rename,
451     changeKernel,
452     enableIPv6,
453     disableBackups,
454     enablePrivateNetworking,
455     snapshot,
456     upgrade,
457 }
458 
459 ///
460 string toString(DropletAction action)
461 {
462     final switch(action) with(DropletAction)
463     {
464         case reboot:
465             return "reboot";
466         case powerCycle:
467             return "power_cycle";
468         case shutdown:
469             return "shutdown";
470         case powerOff:
471             return "power_off";
472         case powerOn:
473             return "power_on";
474         case passwordReset:
475             return "password_reset";
476         case resize:
477             return "resize";
478         case restore:
479             return "restore";
480         case rebuild:
481             return "rebuild";
482         case rename:
483             return "rename";
484         case changeKernel:
485             return "change_kernel";
486         case enableIPv6:
487             return "enable_ipv6";
488         case disableBackups:
489             return "disable_backups";
490         case enablePrivateNetworking:
491             return "enable_private_networking";
492         case snapshot:
493             return "snapshot";
494         case upgrade:
495             return "upgrade";
496     }
497 }
498 
499 
500 ///
501 immutable string[] DropletActions;
502 
503 ///
504 DropletAction dropletAction(string action)
505 {
506     auto i=DropletActions.countUntil(action);
507     enforce(i>=0,new Exception("unknown droplet action: "~action));
508     return cast(DropletAction)i;
509 }
510 
511 ///
512 struct OceanResult(T)
513 {
514     bool found;
515     T result;
516 }
517 
518 /// find droplet ID from anme
519 OceanResult!Droplet findDroplet(OceanAPI ocean, string name)
520 {
521 	import symmetry.helper.prettyjson;
522     auto ret=ocean.Droplet(-1);
523     auto dropletResults=ocean.listDroplets;
524     auto droplets="droplets" in dropletResults;
525     enforce(droplets !is null, new Exception("bad response from Digital Ocean: "~dropletResults.prettyPrint));
526     enforce((*droplets).type==JSONType.array, new Exception
527         ("bad response from Digital Ocean: "~dropletResults.prettyPrint));
528     (*droplets).array.each!(a=>enforce(("name" in a.object) && a.object["name"].type==JSON_TYPE.STRING));
529     auto i=(*droplets).array.map!(a=>a.object["name"].str).array.countUntil(name);
530     if (i==-1)
531     {
532         return OceanResult!Droplet(false,ret);
533     }
534     //auto p=("id" in ((*droplets).array[i]));
535     //enforce(p !is null, new Exception
536       //  ("findDroplet cannot find id in results - malformed JSON?\n"~dropletResults.prettyPrint));
537     return OceanResult!Droplet(true,ocean.Droplet((*droplets).array[i].object["id"].integer.to!int));
538 }
539 
540 ///  List all available Kernels for a Droplet
541 auto kernels(Droplet droplet)
542 {
543     return droplet.request(format("droplets/%s/kernels",droplet.id), HTTP.Method.get);
544 }
545 
546 
547 ///  Retrieve snapshots for a Droplet
548 auto snapshots(Droplet droplet)
549 {
550     return droplet.request(format("droplets/%s/snapshots",droplet.id), HTTP.Method.get);
551 }
552 
553 ///  Retrieve backups for a Droplet
554 auto backups(Droplet droplet)
555 {
556   return droplet.request(format("droplets/%s/backups",droplet.id), HTTP.Method.get);
557 }
558 
559 //  Retrieve actions for a Droplet
560 auto actions(Droplet droplet)
561 {
562     return droplet.request(format("droplets/%s/actions",droplet.id), HTTP.Method.get);
563 }
564 
565 ///  Retrieve an existing Droplet by id
566 auto retrieve(Droplet droplet)
567 {
568     return droplet.request("droplets/"~droplet.id.to!string, HTTP.Method.get);
569 }
570 
571 ///  Delete a Droplet
572 auto del(Droplet droplet)
573 {
574     return droplet.request("droplets/"~droplet.id.to!string, HTTP.Method.del);
575 }
576 
577 ///
578 auto neighbours(Droplet droplet)
579 {
580     return droplet.request(format("droplets/%s/neighbors",droplet.id),HTTP.Method.get);
581 }
582 
583 ///  Reboot a Droplet
584 auto reboot(Droplet droplet)
585 {
586     return droplet.action(DropletAction.reboot);
587 }
588 
589 ///  Power Cycle a Droplet
590 auto powerCycle(Droplet droplet)
591 {
592     return droplet.action(DropletAction.powerCycle);
593 }
594 
595 ///  Shutdown a Droplet
596 auto shutdown(Droplet droplet)
597 {
598     return droplet.action(DropletAction.shutdown);
599 }
600 
601 ///  Power Off a Droplet
602 auto powerOff(Droplet droplet)
603 {
604     return droplet.action(DropletAction.powerOff);
605 }
606 
607 ///  Power On a Droplet
608 auto powerOn(Droplet droplet)
609 {
610     return droplet.action(DropletAction.powerOn);
611 }
612 
613 ///  Password Reset a Droplet
614 auto passwordReset(Droplet droplet)
615 {
616     return droplet.action(DropletAction.passwordReset);
617 }
618 
619 ///  Resize a Droplet
620 auto resize(Droplet droplet, string size)
621 {
622     JSONValue params;
623     params["size"]=size;
624     return droplet.action(DropletAction.resize, params);
625 }
626 
627 ///  Restore a Droplet
628 auto restore(Droplet droplet, string image)
629 {
630     JSONValue params;
631     params["image"]=image;
632     return droplet.action(DropletAction.restore, params);
633 }
634 
635 ///  Rebuild a Droplet
636 auto rebuild(Droplet droplet, string image)
637 {
638     JSONValue params;
639     params["image"]=image;
640     return droplet.action(DropletAction.rebuild, params);
641 }
642 
643 ///  Rename a Droplet
644 auto rename(OceanAPI api, Droplet droplet, string name)
645 {
646     JSONValue params;
647     params["name"]=name;
648     return droplet.action(DropletAction.rename,params);
649 }
650 
651 ///  Change the Kernel
652 auto changeKernel(Droplet droplet, string kernel)
653 {
654     JSONValue params;
655     params["kernel"]=kernel;
656     return droplet.action(DropletAction.changeKernel, params);
657 }
658 
659 ///  Enable IPv6
660 auto enableIPv6(Droplet droplet)
661 {
662     return droplet.action(DropletAction.enableIPv6);
663 }
664 
665 ///  Disable Backups
666 auto disableBackups(Droplet droplet)
667 {
668     return droplet.action(DropletAction.disableBackups);
669 }
670 
671 ///  Enable Private Networking
672 auto enablePrivateNetworking(Droplet droplet)
673 {
674     return droplet.action(DropletAction.enablePrivateNetworking);
675 }
676 
677 ///  Snapshot
678 auto doSnapshot(Droplet droplet, string name=null)
679 {
680     JSONValue params;
681     if (name.length>0)
682         params["name"]=name;
683     return droplet.action(DropletAction.snapshot, params);
684 }
685 
686 ///  Retrieve a Droplet Action
687 auto retrieveAction(Droplet droplet, string actionId)
688 {
689     return droplet.request(
690         format("droplets/%s/actions/%s",droplet.id, actionId), HTTP.Method.get);
691 }
692 
693 ///
694 auto upgrade(Droplet droplet)
695 {
696     JSONValue params;
697     params["upgrade"]=true;
698     droplet.action(DropletAction.upgrade,params);
699 }
700 
701 ///
702 struct OceanImage
703 {
704     OceanAPI api;
705     string id;
706     alias id this;
707     this(OceanAPI api, string id)
708     {
709         this.api=api;
710         this.id=id;
711     }
712     auto request(string uri, HTTP.Method method=HTTP.Method.get, JSONValue params=JSONValue(null))
713     {
714         return api.request(uri,method,params);
715     }
716 }
717 
718 /// List all images
719 auto listImages(OceanAPI api)
720 {
721     return api.request("images", HTTP.Method.get);
722 }
723 
724 /// Retrieve an existing Image by id or slug
725 auto get(OceanImage image)
726 {
727     return image.request("images/"~image.id, HTTP.Method.get);
728 }
729 
730 ///  Delete an Image
731 auto del(OceanImage image)
732 {
733     return image.request("images/"~image.id, HTTP.Method.del);
734 }
735 
736 ///  Update an Image
737 auto update(OceanImage image, string name)
738 {
739     JSONValue params;
740     params["name"]=name;
741     return image.request("images/"~image.id, HTTP.Method.put, params);
742 }
743 
744 ///  Transfer an Image
745 auto transfer(OceanImage image, OceanRegion region)
746 {
747     JSONValue params;
748     params["type"]="transfer";
749     params["region"]=region.to!string;
750     return image.request(format("images/%s/actions",image.id), HTTP.Method.post,params);
751 }
752 
753 /// Retrieve an existing Image Action
754 auto getImageAction(OceanImage image, string actionId)
755 {
756     return image.request(format("images/%s/actions/%s",image.id,actionId), HTTP.Method.get);
757 }
758 
759 ///
760 struct OceanKey
761 {
762     OceanAPI api;
763     string value;
764 
765     ///
766     this(OceanAPI api,string key)
767     {
768         this.api=api;
769         this.value=key;
770     }
771     
772     ///
773     auto request(string uri, HTTP.Method method=HTTP.Method.get, JSONValue params=JSONValue(null))
774     {
775         return api.request(uri,method,params);
776     }
777 
778     /// Create a new Key
779     static auto create(OceanAPI api, string name, string publicKey)
780     {
781         JSONValue params;
782         params["name"]=name;
783         params["public_key"]=publicKey;
784         return api.request("account/keys", HTTP.Method.post, params);
785     }
786 }
787 
788 /// list all keys
789 auto listKeys(OceanAPI api)
790 {
791     return api.request("account/keys", HTTP.Method.get);
792 }
793 
794 
795 /// Retrieve an existing Key by Id or Fingerprint
796 auto retrieve(OceanKey key)
797 {
798     return key.request("account/keys/"~key.value, HTTP.Method.get);
799 }
800 
801 /// Update an existing Key by Id or Fingerprint
802 auto updateName(OceanKey key, string name)
803 {
804     JSONValue params;
805     params["name"]=name;
806     return key.request("account/keys/"~key.value, HTTP.Method.put, params);
807 }
808 
809 ///  Destroy an existing Key by Id or Fingerprint
810 auto del(OceanKey key)
811 {
812     return key.request("account/keys/"~key.value, HTTP.Method.del);
813 }
814 
815 
816 /// list all regions
817 auto listRegions(OceanAPI api)
818 {
819    return api.request("regions", HTTP.Method.get);
820 }
821 
822 /// list all sizes
823 auto listSizes(OceanAPI api)
824 {
825     return api.request("sizes", HTTP.Method.get);
826 }