├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── config.nims ├── digitalocean.nimble ├── src └── digitalocean.nim └── tests ├── config.nims └── test.nim /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - uses: actions/checkout@v1 9 | 10 | - name: Cache choosenim 11 | id: cache-choosenim 12 | uses: actions/cache@v1 13 | with: 14 | path: ~/.choosenim 15 | key: ${{ runner.os }}-choosenim-stable 16 | 17 | - name: Cache nimble 18 | id: cache-nimble 19 | uses: actions/cache@v1 20 | with: 21 | path: ~/.nimble 22 | key: ${{ runner.os }}-nimble-stable 23 | 24 | - uses: jiro4989/setup-nim-action@v1.0.2 25 | 26 | - run: nimble test -y 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore files with no extention: 2 | * 3 | !*/ 4 | !*.* 5 | 6 | # normal ignores: 7 | *.exe 8 | nimcache 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Andre von Houck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wrapper for DigitalOcean HTTP API. 2 | 3 | `nimble install digitalocean` 4 | 5 | ![Github Actions](https://github.com/treeform/digitalocean/workflows/Github%20Actions/badge.svg) 6 | 7 | [API reference](https://nimdocs.com/treeform/digitalocean) 8 | 9 | This library has no dependencies other than the Nim standard library. 10 | 11 | ## About 12 | 13 | Wraps many of the API endpoints that digital ocean has. If you need more of them wrapped feel free to open up an issue or add a pull request. 14 | 15 | DigitalOcean API docs Here: https://developers.digitalocean.com/documentation/v2/ 16 | 17 | 18 | ## Imports 19 | This library really worls well with [ssh](https://github.com/treeform/ssh) and I also use [print](https://github.com/treeform/print). 20 | ```nim 21 | import digitalocean, ssh, print 22 | ``` 23 | Then you need to get your API token from your DigitalOcean account: https://cloud.digitalocean.com/account/api/tokens 24 | ```nim 25 | setToken(token) 26 | ``` 27 | 28 | ## Create a new Droplet 29 | 30 | I created this wrapper to manage servers based on demand. 31 | I use this to spin up servers when they are needed and spin them down 32 | when they are not. 33 | 34 | ```nim 35 | var droplet = await createDroplet( 36 | name = "example server", 37 | region = "sfo2", 38 | size = "s-1vcpu-1gb", 39 | image = 100001, # use getUserImages to get id 40 | ssh_keys = @[10000], # use getSSHKeys to get id 41 | backups = false, 42 | ipv6 = false, 43 | private_networking = false, 44 | user_data = "", 45 | monitoring = false, 46 | volumes = @[], 47 | tags = @["test"], 48 | ) 49 | ``` 50 | 51 | Wait for the droplet to become active: 52 | 53 | ```nim 54 | while droplet.status == "new": 55 | print droplet.status 56 | droplet = await getDroplet(d.id) 57 | sleep(1000) 58 | print dropletd.status 59 | ``` 60 | 61 | After a droplet is `active` it is ready to start doing things. 62 | I is use my [ssh library](https://github.com/treeform/ssh) to log into the computer to set it up. Setup is some thing like this: 63 | 64 | ```nim 65 | import ssh 66 | var server = newSSH(user & "@" & droplet.publicIp) 67 | server.command("apk install ...") 68 | server.writeFile("/etc/someconfig", "{...}") 69 | server.exit() 70 | ``` 71 | 72 | Then delete the droplet when I am done: 73 | 74 | ```nim 75 | await deleteDroplet(droplet.id) 76 | ``` 77 | 78 | ## Get SSH Keys 79 | 80 | I highly recommend using SSH keys for everything, never use server passwords. You can list the keys and the IDs you need for droplet creation with this: 81 | 82 | ```nim 83 | for key in waitFor getSSHKeys(): 84 | echo (key.name, key.id) 85 | ``` 86 | 87 | ## Get Images 88 | 89 | I create an image that is a base setup for all my server. You can use the images you have from here. You need the `image.id` and `image.regions` to match when creating a droplet. 90 | 91 | ```nim 92 | for image in waitFor getUserImages(): 93 | echo (image.name, image.id, image.slug, image.regions) 94 | ``` 95 | 96 | You can also list public images which there are many of: 97 | 98 | ```nim 99 | for image in waitFor getAllImages(): 100 | echo (image.name, image.id, image.slug) 101 | ``` 102 | 103 | ## Get Droplets 104 | 105 | You can see all the droplets you have here: 106 | 107 | ```nim 108 | for droplet in waitFor getAllDroplets(): 109 | echo droplet.name 110 | ``` 111 | 112 | But I think it’s more useful to tag your droplets and look at the by tag. Once you have many droplets the list becomes cluttered. 113 | 114 | ```nim 115 | for droplet in waitFor getDropletsByTag("gameserver"): 116 | echo droplet.name 117 | ``` 118 | 119 | ## Get your Account Information 120 | 121 | You can get your account information. 122 | 123 | ```nim 124 | echo (waitFor getAccount()).email 125 | ``` 126 | 127 | You can also get recent actions that happened like which servers got started or stopped: 128 | 129 | ```nim 130 | for action in waitFor getAllActions(): 131 | echo action.`type` 132 | ``` 133 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | --d:ssl 2 | -------------------------------------------------------------------------------- /digitalocean.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.1" 4 | author = "Andre von Houck" 5 | description = "Wrapper for DigitalOcean HTTP API." 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 0.19.0" 12 | -------------------------------------------------------------------------------- /src/digitalocean.nim: -------------------------------------------------------------------------------- 1 | # Wrapper for Digital Ocean HTTP Api. 2 | 3 | import asyncdispatch, asyncnet, httpclient, json, ospaths, strutils, tables, uri 4 | 5 | type 6 | DigitalOceanError = object of Exception 7 | 8 | Account* = ref object 9 | ## Information about your current account. 10 | droplet_limit*: int # The total number of Droplets current user or team may have active at one time. 11 | floating_ip_limit*: int # The total number of Floating IPs the current user or team may have. 12 | email*: string # The email address used by the current user to registered for DigitalOcean. 13 | uuid*: string # The unique universal identifier for the current user. 14 | email_verified*: bool # If true, the user has verified their account via email. False otherwise. 15 | status*: string # This value is one of "active", "warning" or "locked". 16 | status_message*: string # A human-readable message giving more details about the status of the account. 17 | 18 | Action* = ref object 19 | ## Actions are records of events that have occurred on the resources in your account. These can be things like rebooting a Droplet, or transferring an image to a new region. 20 | id*: int # A unique numeric ID that can be used to identify and reference an action. 21 | status*: string # The current status of the action. This can be "in-progress", "completed", or "errored". 22 | `type`*: string # This is the type of action that the object represents. For example, this could be "transfer" to represent the state of an image transfer action. 23 | started_at*: string # A time value given in ISO8601 combined date and time format that represents when the action was initiated. 24 | completed_at*: string # A time value given in ISO8601 combined date and time format that represents when the action was completed. 25 | resource_id*: int # A unique identifier for the resource that the action is associated with. 26 | resource_type*: string # The type of resource that the action is associated with. 27 | #region*: object # A full region object containing information about the region where the action occurred. 28 | region_slug*: string # A slug representing the region where the action occurred. 29 | 30 | NetworkInterface = object 31 | ip_address: string 32 | netmask: string 33 | gateway: string 34 | `type`: string 35 | 36 | Networks = object 37 | v4: seq[NetworkInterface] 38 | v6: seq[NetworkInterface] 39 | 40 | Droplet* = ref object 41 | ## A Droplet is a DigitalOcean virtual machine. 42 | id*: int # A unique identifier for each Droplet instance. This is automatically generated upon Droplet creation. 43 | name*: string # The human-readable name set for the Droplet instance. 44 | memory*: int # Memory of the Droplet in megabytes. 45 | vcpus*: int # The number of virtual CPUs. 46 | disk*: int # The size of the Droplet's disk in gigabytes. 47 | locked*: bool # A boolean value indicating whether the Droplet has been locked, preventing actions by users. 48 | created_at*: string # A time value given in ISO8601 combined date and time format that represents when the Droplet was created. 49 | status*: string # A status string indicating the state of the Droplet instance. This may be "new", "active", "off", or "archive". 50 | backup_ids*: seq[int] # An array of backup IDs of any backups that have been taken of the Droplet instance. Droplet backups are enabled at the time of the instance creation. 51 | snapshot_ids*: seq[int] # An array of snapshot IDs of any snapshots created from the Droplet instance. 52 | features*: seq[string] # An array of features enabled on this Droplet. 53 | #region*: object # The region that the Droplet instance is deployed in. When setting a region, the value should be the slug identifier for the region. When you query a Droplet, the entire region object will be returned. 54 | #image*: object # The base image used to create the Droplet instance. When setting an image, the value is set to the image id or slug. When querying the Droplet, the entire image object will be returned. 55 | #size*: object # The current size object describing the Droplet. When setting a size, the value is set to the size slug. When querying the Droplet, the entire size object will be returned. Note that the disk volume of a Droplet may not match the size's disk due to Droplet resize actions. The disk attribute on the Droplet should always be referenced. 56 | size_slug*: string # The unique slug identifier for the size of this Droplet. 57 | networks*: Networks # The details of the network that are configured for the Droplet instance. This is an object that contains keys for IPv4 and IPv6. The value of each of these is an array that contains objects describing an individual IP resource allocated to the Droplet. These will define attributes like the IP address, netmask, and gateway of the specific network depending on the type of network it is. 58 | #kernel*: object # The current kernel. This will initially be set to the kernel of the base image when the Droplet is created. 59 | #next_backup_window*: object # The details of the Droplet's backups feature, if backups are configured for the Droplet. This object contains keys for the start and end times of the window during which the backup will start. 60 | tags*: seq[string] # An array of Tags the Droplet has been tagged with. 61 | volume_ids*: seq[string] # A flat array including the unique identifier for each Block Storage volume attached to the Droplet. 62 | 63 | Image* = ref object 64 | id*: int # A unique number that can be used to identify and reference a specific image. 65 | name*: string # The display name that has been given to an image. This is what is shown in the control panel and is generally a descriptive title for the image in question. 66 | `type`*: string # The kind of image, describing the duration of how long the image is stored. This is either "snapshot", "backup", or "custom". 67 | distribution*: string # This attribute describes the base distribution used for this image. For custom images, this is user defined. 68 | slug*: string #string: A uniquely identifying string that is associated with each of the DigitalOcean-provided public images. These can be used to reference a public image as an alternative to the numeric id. 69 | public*: bool # This is a boolean value that indicates whether the image in question is public or not. An image that is public is available to all accounts. A non-public image is only accessible from your account. 70 | regions*: seq[string] # This attribute is an array of the regions that the image is available in. The regions are represented by their identifying slug values. 71 | created_at*: string # A time value given in ISO8601 combined date and time format that represents when the image was created. 72 | min_disk_size*: int # The minimum disk size in GB required for a Droplet to use this image. 73 | size_gigabytes*: float # The size of the image in gigabytes. 74 | description*: string # An optional free-form text field to describe an image. 75 | tags*: seq[string] # An array containing the names of the tags the image has been tagged with. 76 | status*: string # A status string indicating the state of a custom image. This may be "NEW", "available", "pending", or "deleted". 77 | error_message*: string # A string containing information about errors that may occur when importing a custom image. 78 | 79 | SSHKey* = ref object 80 | id*: int # This is a unique identification number for the key. This can be used to reference a specific SSH key when you wish to embed a key into a Droplet. 81 | fingerprint*: string # This attribute contains the fingerprint value that is generated from the public key. This is a unique identifier that will differentiate it from other keys using a format that SSH recognizes. 82 | public_key*: string # This attribute contains the entire public key string that was uploaded. This is what is embedded into the root user's authorized_keys file if you choose to include this SSH key during Droplet creation. 83 | name*: string # This is the human-readable display name for the given SSH key. This is used to easily identify the SSH keys when they are displayed. 84 | 85 | DomainRecord* = ref object 86 | id*: int # A unique identifier for each domain record. 87 | `type`*: string # The type of the DNS record. For example: A, CNAME, TXT, ... 88 | name*: string # The host name, alias, or service being defined by the record. 89 | data*: string # Variable data depending on record type. For example, the "data" value for an A record would be the IPv4 address to which the domain will be mapped. For a CAA record, it would contain the domain name of the CA being granted permission to issue certificates. 90 | # priority*: int # The priority for SRV and MX records. 91 | # port*: int # The port for SRV records. 92 | ttl*: int # This value is the time to live for the record, in seconds. This defines the time frame that clients can cache queried information before a refresh should be requested. 93 | # weight*: int # The weight for SRV records. 94 | # flags*: int # An unsigned integer between 0-255 used for CAA records. 95 | # tag*: string # The parameter tag for CAA records. Valid values are "issue", "issuewild", or "iodef" 96 | 97 | ResourceType* = enum 98 | droplet 99 | image 100 | volume 101 | volume_snapshot 102 | 103 | proc checkForError(json: JsonNode) = 104 | if "id" in json and "message" in json: 105 | raise newException( 106 | DigitalOceanError, 107 | json["id"].getStr() & ": " & json["message"].getStr() 108 | ) 109 | 110 | proc ipv4(droplet: Droplet, networkType: string): string = 111 | for net in droplet.networks.v4: 112 | if net.`type` == networkType: 113 | return net.ip_address 114 | 115 | proc publicIp*(droplet: Droplet): string = 116 | ## Given a droplet finds its public v4 ip_address in networks object 117 | droplet.ipv4("public") 118 | 119 | proc privateIp*(droplet: Droplet): string = 120 | ## Given a droplet finds its private v4 ip_address in networks object 121 | droplet.ipv4("private") 122 | 123 | const apiEndpoint = "https://api.digitalocean.com" 124 | var globalToken: string 125 | 126 | proc encodePostBody(params: openarray[(string, string)]): string = 127 | var parts = newSeq[string]() 128 | for pair in params: 129 | parts.add(encodeUrl(pair[0]) & "=" & encodeUrl(pair[1])) 130 | parts.join("&") 131 | 132 | proc encodeParams(url: string, params: openarray[(string, string)]): string = 133 | url & "?" & encodePostBody(params) 134 | 135 | proc setToken*(token: string) = 136 | globalToken = token 137 | 138 | proc getAccount*(): Future[Account] {.async.} = 139 | let client = newAsyncHttpClient() 140 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 141 | let response = await client.get(apiEndpoint & "/v2/account") 142 | return to(parseJson(await response.body)["account"], Account) 143 | 144 | proc getAllActions*(): Future[seq[Action]] {.async.} = 145 | let client = newAsyncHttpClient() 146 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 147 | let response = await client.get(apiEndpoint & "/v2/actions") 148 | let json = parseJson(await response.body) 149 | var actions = newSeq[Action]() 150 | for accountJson in json["actions"]: 151 | actions.add(to(accountJson, Action)) 152 | return actions 153 | 154 | proc getAction*(actionId: int): Future[Action] {.async.} = 155 | let client = newAsyncHttpClient() 156 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 157 | let response = await client.get(apiEndpoint & "/v2/actions/" & $actionId) 158 | return to(parseJson(await response.body)["action"], Action) 159 | 160 | ## Droplets 161 | 162 | proc getAllDroplets*(page = 1, per_page = 100): Future[seq[Droplet]] {.async.} = 163 | let client = newAsyncHttpClient() 164 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 165 | let response = await client.get(encodeParams( 166 | apiEndpoint & "/v2/droplets", {"page": $page, "per_page": $per_page})) 167 | let json = parseJson(await response.body) 168 | var droplets = newSeq[Droplet]() 169 | for dropletJson in json["droplets"]: 170 | droplets.add(to(dropletJson, Droplet)) 171 | return droplets 172 | 173 | proc getDropletsByTag*(tag: string, page = 1, per_page = 100): Future[seq[ 174 | Droplet]] {.async.} = 175 | let client = newAsyncHttpClient() 176 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 177 | let response = await client.get(encodeParams( 178 | apiEndpoint & "/v2/droplets", 179 | {"page": $page, "per_page": $per_page, "tag_name": tag 180 | })) 181 | let json = parseJson(await response.body) 182 | var droplets = newSeq[Droplet]() 183 | for dropletJson in json["droplets"]: 184 | droplets.add(to(dropletJson, Droplet)) 185 | return droplets 186 | 187 | proc getDroplet*(dropletId: int): Future[Droplet] {.async.} = 188 | let client = newAsyncHttpClient() 189 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 190 | let response = await client.get(apiEndpoint & "/v2/droplets/" & $dropletId) 191 | let json = parseJson(await response.body) 192 | let droplets = newSeq[Droplet]() 193 | let dropletJson = json["droplet"] 194 | return to(dropletJson, Droplet) 195 | 196 | const dropletSizes = [ 197 | "s-1vcpu-1gb", 198 | "s-1vcpu-2gb", 199 | "s-1vcpu-3gb", 200 | "s-2vcpu-2gb", 201 | "s-3vcpu-1gb", 202 | "s-2vcpu-4gb", 203 | "s-4vcpu-8gb", 204 | "s-6vcpu-16gb", 205 | "s-8vcpu-32gb", 206 | "s-12vcpu-48gb", 207 | "s-16vcpu-64gb", 208 | "s-20vcpu-96gb", 209 | "s-24vcpu-128gb", 210 | "s-32vcpu-192gb" 211 | ] 212 | 213 | proc createDroplet*( 214 | name: string, # The human-readable string you wish to use when displaying the Droplet name. 215 | region: string, # The unique slug identifier for the region that you wish to deploy in. true 216 | size: string, # The unique slug identifier for the size that you wish to select for this Droplet. true 217 | image: int, # if using an image ID), or String (if using a public image slug) The image ID of a public or private image, or the unique slug identifier for a public image. This image will be the base image for your Droplet. 218 | ssh_keys: seq[int], # An array containing the IDs or fingerprints of the SSH keys that you wish to embed in the Droplet's root account upon creation. 219 | backups: bool, # A boolean indicating whether automated backups should be enabled for the Droplet. Automated backups can only be enabled when the Droplet is created. 220 | ipv6: bool, # A boolean indicating whether IPv6 is enabled on the Droplet. 221 | private_networking: bool, # A boolean indicating whether private networking is enabled for the Droplet. Private networking is currently only available in certain regions. 222 | user_data: string, # A string containing 'user data' which may be used to configure the Droplet on first boot, often a 'cloud-config' file or Bash script. It must be plain text and may not exceed 64 KiB in size. 223 | monitoring: bool, # A boolean indicating whether to install the DigitalOcean agent for monitoring. 224 | volumes: seq[string], # A flat array including the unique string identifier for each Block Storage volume to be attached to the Droplet. At the moment a volume can only be attached to a single Droplet. 225 | tags: seq[string], # A flat array of tag names as strings to apply to the Droplet after it is created. Tag names can either be existing or new tags. 226 | ): Future[Droplet] {.async.} = 227 | let client = newAsyncHttpClient() 228 | client.headers = newHttpHeaders({ 229 | "Authorization": "Bearer " & globalToken, 230 | "Content-Type": "application/json" 231 | }) 232 | let bodyStr = $(%*{ 233 | "name": name, 234 | "region": region, 235 | "size": size, 236 | "image": image, 237 | "ssh_keys": ssh_keys, 238 | "backups": backups, 239 | "ipv6": ipv6, 240 | "private_networking": private_networking, 241 | "user_data": user_data, 242 | "monitoring": monitoring, 243 | "volumes": volumes, 244 | "tags": tags 245 | }) 246 | let response = await client.post(apiEndpoint & "/v2/droplets", body = bodyStr) 247 | let json = parseJson(await response.body) 248 | checkForError(json) 249 | let droplets = newSeq[Droplet]() 250 | let dropletJson = json["droplet"] 251 | return to(dropletJson, Droplet) 252 | 253 | proc deleteDroplet*(dropletId: int) {.async.} = 254 | let client = newAsyncHttpClient() 255 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 256 | let response = await client.request( 257 | apiEndpoint & "/v2/droplets/" & $dropletId, httpMethod = HttpDelete) 258 | if response.status != "204 No Content": 259 | raise newException(DigitalOceanError, "Droplet was not deleted") 260 | 261 | ## Images 262 | 263 | proc getImages*(url: string): Future[seq[Image]] {.async.} = 264 | let client = newAsyncHttpClient() 265 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 266 | let response = await client.get(url) 267 | let json = parseJson(await response.body) 268 | var images = newSeq[Image]() 269 | for dropletJson in json["images"]: 270 | images.add(to(dropletJson, Image)) 271 | return images 272 | 273 | proc getAllImages*(page = 1, per_page = 100): Future[seq[Image]] {.async.} = 274 | return await getImages(encodeParams( 275 | apiEndpoint & "/v2/images", {"page": $page, "per_page": $per_page})) 276 | 277 | proc getDistributionImages*(page = 1, per_page = 100): Future[seq[Image]] {.async.} = 278 | return await getImages(encodeParams(apiEndpoint & "/v2/images", { 279 | "page": $page, "per_page": $per_page, "type": "distribution"})) 280 | 281 | proc getApplicationImages*(page = 1, per_page = 100): Future[seq[Image]] {.async.} = 282 | return await getImages(encodeParams(apiEndpoint & "/v2/images", { 283 | "page": $page, "per_page": $per_page, "type": "application"})) 284 | 285 | proc getUserImages*(page = 1, per_page = 100): Future[seq[Image]] {.async.} = 286 | return await getImages(encodeParams(apiEndpoint & "/v2/images", { 287 | "page": $page, "per_page": $per_page, "private": "true"})) 288 | 289 | proc getImagesByTag*(tag: string, page = 1, per_page = 100): Future[seq[ 290 | Image]] {.async.} = 291 | return await getImages(encodeParams(apiEndpoint & "/v2/images", { 292 | "page": $page, "per_page": $per_page, "tag": tag})) 293 | 294 | ## SSH Keys 295 | 296 | proc getSSHKeys*(page = 1, per_page = 100): Future[seq[SSHKey]] {.async.} = 297 | let client = newAsyncHttpClient() 298 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 299 | let response = await client.get(encodeParams( 300 | apiEndpoint & "/v2/account/keys", {"page": $page, "per_page": $per_page})) 301 | let json = parseJson(await response.body) 302 | var keys = newSeq[SSHKey]() 303 | for keysJson in json["ssh_keys"]: 304 | keys.add(to(keysJson, SSHKey)) 305 | return keys 306 | 307 | ## Domain Records 308 | 309 | proc getAllDomainRecords*( 310 | domainName: string, 311 | page = 1, 312 | per_page = 100 313 | ): Future[seq[DomainRecord]] {.async.} = 314 | let client = newAsyncHttpClient() 315 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 316 | let response = await client.get(encodeParams( 317 | apiEndpoint & "/v2/domains/" & domainName & "/records", 318 | {"page": $page, "per_page": $per_page} 319 | )) 320 | let json = parseJson(await response.body) 321 | result = newSeq[DomainRecord]() 322 | for j in json["domain_records"]: 323 | result.add(to(j, DomainRecord)) 324 | 325 | proc createDomainRecord*( 326 | domainName: string, 327 | kind: string, 328 | name: string, 329 | data: string, 330 | ttl: int = 3600 331 | ): Future[DomainRecord] {.async.} = 332 | let client = newAsyncHttpClient() 333 | client.headers = newHttpHeaders({ 334 | "Authorization": "Bearer " & globalToken, 335 | "Content-Type": "application/json" 336 | }) 337 | let bodyStr = $(%*{ 338 | "name": name, 339 | "type": kind, 340 | "data": data, 341 | "ttl": ttl 342 | }) 343 | let response = await client.post( 344 | apiEndpoint & "/v2/domains/" & domainName & "/records", 345 | body = bodyStr 346 | ) 347 | let json = parseJson(await response.body) 348 | checkForError(json) 349 | let j = json["domain_record"] 350 | return to(j, DomainRecord) 351 | 352 | proc deleteDomainRecord*(domainName: string, domainRecordId: int) {.async.} = 353 | let client = newAsyncHttpClient() 354 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 355 | let response = await client.request( 356 | apiEndpoint & "/v2/domains/" & domainName & "/records/" & $domainRecordId, 357 | httpMethod = HttpDelete 358 | ) 359 | if response.status != "204 No Content": 360 | raise newException(DigitalOceanError, "Domain record was not deleted") 361 | 362 | ## Tags 363 | 364 | proc `%`(t: tuple[resource_id: string, resource_type: ResourceType]): JsonNode = 365 | %*{ 366 | "resource_id": t.resource_id, 367 | "resource_type": $t.resource_type 368 | } 369 | 370 | proc createTag*(tag: string) {.async.} = 371 | let client = newAsyncHttpClient() 372 | client.headers = newHttpHeaders({ 373 | "Authorization": "Bearer " & globalToken, 374 | "Content-Type": "application/json" 375 | }) 376 | let bodyStr = $(%*{ 377 | "name": tag 378 | }) 379 | let response = await client.post(apiEndpoint & "/v2/tags", body = bodyStr) 380 | let json = parseJson(await response.body) 381 | checkForError(json) 382 | 383 | proc deleteTag*(tag: string) {.async.} = 384 | let client = newAsyncHttpClient() 385 | client.headers = newHttpHeaders({"Authorization": "Bearer " & globalToken}) 386 | let response = await client.request( 387 | apiEndpoint & "/v2/tags/" & tag, 388 | httpMethod = HttpDelete 389 | ) 390 | if response.status != "204 No Content": 391 | raise newException(DigitalOceanError, "Domain record was not deleted") 392 | 393 | proc tagResources*( 394 | tag: string, 395 | resources: seq[tuple[resource_id: string, resource_type: ResourceType]] 396 | ) {.async.} = 397 | let client = newAsyncHttpClient() 398 | client.headers = newHttpHeaders({ 399 | "Authorization": "Bearer " & globalToken, 400 | "Content-Type": "application/json" 401 | }) 402 | let bodyStr = $(%*{ 403 | "resources": resources 404 | }) 405 | let response = await client.post( 406 | apiEndpoint & "/v2/tags/" & tag & "/resources", 407 | body = bodyStr 408 | ) 409 | let rb = await response.body 410 | if rb.len != 0: 411 | let json = parseJson(rb) 412 | checkForError(json) 413 | 414 | proc untagResources*(tag: string, resources: seq[tuple[resource_id: string, 415 | resource_type: ResourceType]]) {.async.} = 416 | let client = newAsyncHttpClient() 417 | client.headers = newHttpHeaders({ 418 | "Authorization": "Bearer " & globalToken, 419 | "Content-Type": "application/json" 420 | }) 421 | let bodyStr = $(%*{ 422 | "resources": resources 423 | }) 424 | let response = await client.request( 425 | apiEndpoint & "/v2/tags/" & tag & "/resources", 426 | body = bodyStr, 427 | httpMethod = HttpDelete 428 | ) 429 | let rb = await response.body 430 | if rb.len != 0: 431 | let json = parseJson(rb) 432 | checkForError(json) 433 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | --path:"../src" 2 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import digitalocean, asyncdispatch, os 2 | 3 | let tokenPath = "~/.digital_ocean_token.txt".expandTilde() 4 | 5 | if existsFile(tokenPath): 6 | setToken(readFile(tokenPath)) 7 | 8 | for key in waitFor getSSHKeys(): 9 | echo (key.name, key.id) 10 | 11 | for image in waitFor getUserImages(): 12 | echo (image.name, image.id, image.slug, image.regions) 13 | 14 | for image in waitFor getAllImages(): 15 | echo (image.name, image.id, image.slug) 16 | 17 | for droplet in waitFor getAllDroplets(): 18 | echo droplet.name 19 | 20 | for droplet in waitFor getDropletsByTag("gameserver"): 21 | echo droplet.name 22 | 23 | echo (waitFor getAccount()).email 24 | 25 | for action in waitFor getAllActions(): 26 | echo action.`type` 27 | --------------------------------------------------------------------------------