├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── hetznercloud ├── __init__.py ├── actions.py ├── client.py ├── constants.py ├── datacenters.py ├── exceptions.py ├── floating_ips.py ├── images.py ├── isos.py ├── locations.py ├── server_types.py ├── servers.py ├── shared.py └── ssh_keys.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── base.py ├── shared.py ├── test_configurations.py ├── test_datacenters.py ├── test_floating_ips.py ├── test_images.py ├── test_isos.py ├── test_locations.py ├── test_server_types.py └── test_servers.py /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | .idea/ 3 | #### python #### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | .vscode/ 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | .static_storage/ 59 | .media/ 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | env: 5 | secure: "Ntp8u8TsUVC3ZtAggnCNzZsOEeOCxL6NGit6kt1Be/k6wD2FDaM1xhEBl7rF7KWO1pwrY1JgYve6S3UsxppGnhMWuX64AbfiUqBY9KCZ2zvtAFH2cQC/srrsF83KGurYfP9iiZnlc53KzAkTVbzoI97nQX9VgR/D48C7s6LwzpCoBG9B0JKJ25EbogIy2wWZKUErkhBaSnph8nvew8X76lekUlfiau88bAuSGFWBlR3NYraWzakB4N5IBK2YLBMcmBNh6XW1sbR3RZzwfHZMZYFrfPABjQnE1RS6tXkBoKS2VwtohOR1C2tQJF7T2sA/tv2zM+8lVFUlLwsmpHMUyl4kvpEw8QYjorVhv3zUl4tt8xbnQqNtswpae0vU/+LVh8PAIWAy2I0YTdaCFFOZWViMH03zFrl9vLwDoC8fH59RCFfc2wbKCkRCLXrVUXa9yHBKWk214OBvf36gXp2zir+fj22pTozhu7fjwJsLNn7a4ZddWUxd9/Z+p8JNK//RrECDANMTEb0eRGVQg1f/yExh7Yg8BBh1ZUyhO0DGj4sA9tq+cGjx/LX5dc2WJVgTUorSn1eW+fHkS6rAD34fpCNLdKm1e2ustwJj7nP3cHtEBs/eeqNELRbtx2LEI14iaxF9DMIaonnORxj/3mnwMxt2Ze5cbpl/Gqr627dA9y8=" 6 | install: 7 | - pip install -r requirements.txt 8 | script: 9 | - nosetests -v 10 | deploy: 11 | provider: pypi 12 | user: lssoftware 13 | password: 14 | secure: "ncqUFUf9T+Mc8xCA1kb2YSHk8wK1DVTCHfpokZWZ7uVjIoZiHosZfitPA7MFsyWbeYfgZGhdRvk8WsStBujPtvVTRTSaRKWNLuz90bdb/BUwrjH079jYHRDuDp6qonOYIysn4PvXPoSrOw6hZGNGzV1GU7QAWuiLpVqBc0Zoepfi7AG7lAeM2WYNBW1KAEd91O+WGYI8PfH66ULH2a50eO2TdMGbUEu3SGJwgI4/D7SxBVM114OC80sgjG1+eX/8giK1Fy2S2dwvKKE09h601G8Ia+7YB05VNyX1Sye/27cPnfJs43cMF5NCCgmskghzpqXfhLE0EDDtqr+5hrmpQWlN6/4+6xa0VtjQC7sgeMIYnIlFT08fOSdokpV891iOVAMR/YbR6YVz0LRcEXU85qrm4yezkSpEUb70EFC96QJcnFXkXCHi31IrnWq+C8jln2pjT3Y9B2jlDekNbH9MKjeEIZzcqrpq4akq38I65Yqi76FViMERqjVBfE/97XoIsn3TZvKogqynNdC3b6UWSdbwD7lKglK+emZa0q8nDB2DKR3daYv8BVT+HL1KuuCbZRhKIWeG71L2sDK76gN4HZO8/Lf4TjBDg1TybBW168CXYAuppMulDGIjTPgBmvj+a4WMiROhiNQDjE31zK9xE40VtegzjOhOzVWFeoAarXw=" 15 | on: 16 | tags: true -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 Liam Symonds 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 6 | persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 9 | Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hetzner Cloud Python SDK 2 | 3 | ## DEPRECATED 4 | 5 | THIS LIBRARY IS NO LONGER SUPPORTED. IT WILL RECEIVE NO UPDATES, NO SECURITY FIXES AND NO BUG FIXES. YOU SHOULD BE USING THE OFFICIAL HETZNERCLOUD PYTHON LIBRARY WHICH WAS RELEASED OVER 8 MONTHS AGO. 6 | 7 | PROCEED AT YOUR OWN RISK! 8 | 9 | 10 | 11 | --- 12 | 13 | 14 | 15 | 16 | 17 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build Status](https://travis-ci.org/thlisym/hetznercloud-py.svg?branch=develop)](https://travis-ci.org/elsyms/hetznercloud-py) 18 | 19 | A Python 3 SDK for the new (and wonderful) Hetzner cloud service. 20 | 21 | * [Contributing](#contributing) 22 | * [Changelog](#changelog) 23 | * [Usage](#documentation) 24 | * [Installation](#installation) 25 | * [Getting started](#getting-started) 26 | * [A note on actions](#a-note-on-actions) 27 | * [Standard exceptions](#standard-exceptions) 28 | * [Constants](#constants) 29 | * [Server Statuses](#server-statuses) 30 | * [Action statuses](#action-statuses) 31 | * [Server types](#server-types) 32 | * [Images](#image-types) 33 | * [Datacenters](#datacenter-constants) 34 | * [Backup windows](#backup-windows) 35 | * [Image types](#image-types) 36 | * [Image sorts](#image-sorts) 37 | * [Datacenters](#datacenters) 38 | * [Top level actions](#datacenter-top-level-actions) 39 | * [Get all datacenters](#get-all-datacenters) 40 | * [Get all datacenters by name](#get-all-datacenters-by-name) 41 | * [Get a datacenter by id](#get-datacenter-by-id) 42 | * [Floating IPs](#floating-ips) 43 | * [Top level actions](#floating-ips-top-level-actions) 44 | * [Create](#create-floating-ip) 45 | * [Get all floating IPs](#get-all-floating-ips) 46 | * [Get floating IPs by id](#get-floating-ip-by-id) 47 | * [Modifier actions](#floating-ips-modifier-actions) 48 | * [Assign to server](#assign-floating-ip-to-server) 49 | * [Change description](#change-floating-ip-description) 50 | * [Change reverse DNS entry](#change-floating-ip-reverse-dns-entry) 51 | * [Delete floating IP](#delete-floating-ip) 52 | * [Unassign from server](#unassign-floating-ip-from-server) 53 | * [Images](#images) 54 | * [Top level actions](#images-top-level-actions) 55 | * [Get all images](#get-all-images) 56 | * [Get image by id](#get-image-by-id) 57 | * [Modifier actions (applies to a specific image)](#image-modifier-actions) 58 | * [Update image](#update-image) 59 | * [Delete image](#delete-image) 60 | * [Isos](#isos) 61 | * [Top level actions](#isos-top-level-actions) 62 | * [Get all isos](#get-all-isos) 63 | * [Get all isos by name](#get-all-isos-by-name) 64 | * [Get an iso by id](#get-iso-by-id) 65 | * [Locations](#locations) 66 | * [Top level actions](#locations-top-level-actions) 67 | * [Get all locations](#get-all-locations) 68 | * [Get all locations by name](#get-all-locations-by-name) 69 | * [Get a location by id](#get-location-by-id) 70 | * [Servers](#servers) 71 | * [Top level actions](#server-top-level-actions) 72 | * [Get all servers](#get-all-servers) 73 | * [Get all servers by name](#get-all-servers-by-name) 74 | * [Get server by id](#get-server-by-id) 75 | * [Create server](#create-server) 76 | * [Modifier actions (applies to a specific server)](#server-modifier-actions) 77 | * [Attach an ISO](#attach-iso) 78 | * [Change reverse DNS](#change-reverse-dns) 79 | * [Change name](#change-server-name) 80 | * [Change type](#change-server-type) 81 | * [Delete](#delete-server) 82 | * [Detach ISO](#detach-iso) 83 | * [Disable rescue mode](#disable-rescue-mode) 84 | * [Enable backups](#enable-server-backups) 85 | * [Enable rescue mode](#enable-rescue-mode) 86 | * [Image](#image-server) 87 | * [Power on](#power-on) 88 | * [Power off](#power-off) 89 | * [Rebuild from image](#rebuild-from-image) 90 | * [Reset](#reset-server) 91 | * [Reset root password](#reset-root-password) 92 | * [Shutdown](#shutdown-server) 93 | * [Wait for status](#wait-for-server-status) 94 | * [Server types](#server-size-types) 95 | * [Top level actions](#server-types-top-level-actions) 96 | * [Get all server types](#get-all-server-types) 97 | * [Get all server types by name](#get-all-server-types-by-name) 98 | * [Get a server type by id](#get-server-type-by-id) 99 | * [SSH Keys](#ssh-keys) 100 | * [Top level actions](#ssh-keys-top-level-actions) 101 | * [Create SSH key](#create-ssh-key) 102 | * [Get all SSH keys](#get-all-ssh-keys) 103 | * [Get all SSH keys by name](#get-all-ssh-keys-by-name) 104 | * [Get SSH key by id](#get-ssh-key-by-id) 105 | * [Modifier actions](#ssh-modifier-actions) 106 | * [Delete SSH key](#delete-ssh-key) 107 | * [Update SSH key](#update-ssh-key) 108 | 109 | ## Contributing 110 | 111 | Open source contributions are more than welcome to be submitted to this repository and every issue and pull request will be 112 | promptly reviewed and evaluated on its suitability to be merged into the main branches. 113 | 114 | ## Changelog 115 | 116 | ### v1.1.1 117 | 118 | * Adds new server type constants 119 | * Adds new image constants 120 | * Parses the IP field from the created floating IP API calls 121 | * Fixes the cloud-init documentation. 122 | 123 | ### v1.1.0 124 | 125 | * Allows new servers to be created in any datacenter in a selected location. See [pull request 16](). 126 | 127 | ### v1.0.4 128 | 129 | * Adds the new image type constants. See [pull request 17](https://github.com/elsyms/hetznercloud-py/pull/17). 130 | 131 | ### v1.0.3 132 | 133 | * Fixes an issue with the 8GB server constants having the wrong type. See [issue 15](https://github.com/elsyms/hetznercloud-py/issues/15) 134 | 135 | ### v1.0.2 136 | 137 | * Updated the README to show supported Python version and this changelog section. 138 | * Fixed an issue with status codes returned from server actions. 139 | * Fixed an issue where the body is null when sending a POST request. 140 | * Added the new Helsinki DC as a constant. 141 | * Added a check to the shared API requestor that checks for the 429 rate limited status code and returns a suitable exception. 142 | * Added an alias for datacenters, as my Britishness got the best of me when creating this library. 143 | 144 | ## Documentation 145 | 146 | This library is organised into two components: the first, a set of actions that retrieve an actionable component. 147 | These actionable components (for example servers) each have their own methods that modify their own (and only their own) 148 | behaviour. 149 | 150 | ### Installation 151 | 152 | Install the library into your environment by executing the following command in your terminal: 153 | 154 | ```bash 155 | pip install hetznercloud 156 | ``` 157 | 158 | ### Getting started 159 | 160 | In order to get started with the library, you first need an instance of it. This is very simple, and uses a number 161 | of hand-crafted helper functions to make configuration changes. 162 | 163 | ```python 164 | from hetznercloud import HetznerCloudClientConfiguration, HetznerCloudClient 165 | 166 | configuration = HetznerCloudClientConfiguration().with_api_key("YOUR-API-KEY").with_api_version(1) 167 | client = HetznerCloudClient(configuration) 168 | ``` 169 | 170 | The client is your entry point to the SDK. 171 | 172 | #### A note on actions 173 | 174 | Methods that modify server state (such as creating, imaging, snapshotting, deleting etc) generally return a tuple. The 175 | first value of this tuple is normally the object type modified (i.e. a server), whilst the second value is the action. 176 | 177 | The `HetznerCloudAction` object (second value in the tuple) can be used to wait for certain states of that invoked task 178 | to be achieved before continuing. For more information, see the [Actions](#actions) section. 179 | 180 | #### Constants 181 | 182 | The SDK contains a number of helpful constants that save you having to look up things such as image names, server types 183 | and so on. 184 | 185 | *Please note:* I endeavour to keep the constants up to date, but sometimes it is just not possible. Should 186 | this happen, feel free to use plain-ole strings (i.e. cx11 for the smallest cloud instance) in their place until I get 187 | around to deploying the update. 188 | 189 | ##### Server statuses 190 | 191 | Constants that represent the different statuses a server can have. 192 | 193 | * `SERVER_STATUS_RUNNING` - The server is running. 194 | * `SERVER_STATUS_INITIALIZING` - The server is running its initialisation cycle. 195 | * `SERVER_STATUS_STARTING` - The server is starting after being powered off. 196 | * `SERVER_STATUS_STOPPING` - The server is stopping (shutting down). 197 | * `SERVER_STATUS_OFF` - The server is turned off. 198 | * `SERVER_STATUS_DELETING` - The server is being deleted. 199 | * `SERVER_STATUS_MIGRATING` - The server is being migrated. 200 | * `SERVER_STATUS_REBUILDING` - The server is being rebuilt. 201 | * `SERVER_STATUS_UNKNOWN` - The status of the server is unknown.` 202 | 203 | ##### Action statuses 204 | 205 | Constants that represent the different statuses an action can have. 206 | 207 | * `ACTION_STATUS_RUNNING` - The action is currently running. 208 | * `ACTION_STATUS_SUCCESS` - The action completed successfully. 209 | * `ACTION_STATUS_ERROR` - The action terminated due to an error. 210 | 211 | ##### Server types 212 | 213 | Constants that represent the server types available to users. 214 | 215 | * `SERVER_TYPE_1CPU_2GB` - The smallest type with 1 CPU core, 2GB of RAM and a 20GB SSD disk. 216 | * `SERVER_TYPE_1CPU_2GB_CEPH` - The same as above but with a 20GB CEPH network-attached disk. 217 | * `SERVER_TYPE_2CPU_4GB` - Type with 2 CPU cores, 4GB of RAM and a 40GB SSD disk. 218 | * `SERVER_TYPE_2CPU_4GB_CEPH` - The same as above but with a 40GB CEPH network-attached disk. 219 | * `SERVER_TYPE_2CPU_8GB` - Type with 2 CPU cores, 8GB of RAM and a 80GB SSD disk. 220 | * `SERVER_TYPE_2CPU_8GB_CEPH` - The same as above but with a 80GB CEPH network-attached disk. 221 | * `SERVER_TYPE_4CPU_16GB` - Type with 4 CPU cores, 16GB of RAM and a 160GB SSD disk. 222 | * `SERVER_TYPE_4CPU_16GB_CEPH` - The same as above but with a 160GB CEPH network-attached disk. 223 | * `SERVER_TYPE_8CPU_32GB` - The largest type with 8 CPU cores, 32GB of RAM and a 240GB SSD disk. 224 | * `SERVER_TYPE_8CPU_32GB_CEPH` - The same as above but with a 240GB CEPH network-attached disk. 225 | 226 | 227 | * `SERVER_TYPE_2CPU_8GB_DVCPU` - 2 dedicated CPU cores, 8GB of RAM and 80GB SSD disk. 228 | * `SERVER_TYPE_4CPU_16GB_DVCPU` - 4 dedicated CPU cores, 16GB of RAM and 160GB SSD disk. 229 | * `SERVER_TYPE_8CPU_32GB_DVCPU` - 8 dedicated CPU cores, 32GB of RAM and 240GB SSD disk. 230 | * `SERVER_TYPE_16CPU_64GB_DVCPU` - 16 dedicated CPU cores, 64GB of RAM and 360GB SSD disk. 231 | * `SERVER_TYPE_32CPU_128GB_DVCPU` - 32 dedicated CPU cores, 128GB of RAM and 540GB SSD disk. 232 | 233 | ##### Image types 234 | 235 | Constants that represent the standard images available to users. 236 | 237 | * `IMAGE_UBUNTU_1604` - Ubuntu 16.04 LTS 238 | * `IMAGE_UBUNTU_1804` - Ubuntu 18.04 LTS 239 | * `IMAGE_DEBIAN_9` - Debian 9.3 240 | * `IMAGE_CENTOS_7` - CentOS 7.5 241 | * `IMAGE_FEDORA_27` - Fedora 27 242 | * `IMAGE_FEDORA_28` - Fedora 28 243 | 244 | ##### Datacentre constants 245 | 246 | Constants that represent the datacentres available to users. 247 | 248 | * `DATACENTER_FALKENSTEIN_1` - Falkenstein 1 DC 8 249 | * `DATACENTER_NUREMBERG_1` - Nuremberg 1 DC 3 250 | * `DATACENTER_HELSINKI_1` - Helsinki 1 DC 2 251 | 252 | ##### Backup windows 253 | 254 | Constants that represent backup windows. 255 | 256 | * `BACKUP_WINDOW_10PM_2AM` - Backup window between 10PM and 2AM 257 | * `BACKUP_WINDOW_2AM_6AM` - Backup window between 2AM and 6AM 258 | * `BACKUP_WINDOW_6AM_10AM` - Backup window between 6AM and 10AM 259 | * `BACKUP_WINDOW_10AM_2PM` - Backup window between 10AM and 2PM 260 | * `BACKUP_WINDOW_2PM_6PM` - Backup window between 2PM and 6PM 261 | * `BACKUP_WINDOW_6PM_10PM` - Backup window between 6PM and 10PM 262 | 263 | ##### Floating IP types 264 | 265 | Constants that represent floating IP types 266 | 267 | * `FLOATING_IP_TYPE_IPv4` - IPv4 floating IP 268 | * `FLOATING_IP_TYPE_IPv6` - IPv6 floating IP 269 | 270 | ##### Image types 271 | 272 | Constants that represent image types. 273 | 274 | * `IMAGE_TYPE_BACKUP` - A manual backup of a server, which is then bound to the server it was created from. 275 | * `IMAGE_TYPE_SNAPSHOT` - A snapshot of a server that can be used to create other servers. 276 | 277 | ##### Image sorts 278 | 279 | Constants that define the different ways that images can be sorted. 280 | 281 | * `SORT_BY_ID_ASC` - Sorts the images by their numerical id in ascending order. 282 | * `SORT_BY_ID_DESC` - Sorts the images by their numerical id in descending order. 283 | * `SORT_BY_NAME_ASC` - Sorts the images by their name in ascending character order. 284 | * `SORT_BY_NAME_DESC` - Sorts the images by their name in descending character order. 285 | * `SORT_BY_CREATED_ASC` - Sorts the images by their created date in ascending order. 286 | * `SORT_BY_CREATED_DESC` - Sorts the images by their created date in descending order. 287 | 288 | #### Standard exceptions 289 | 290 | A number of standard exceptions can be thrown from the methods that interact with the Hetzner API. 291 | 292 | * `HetznerAuthenticationException` - raised when the API returns a 401 Not Authorized or 403 Forbidden status code. 293 | * `HetznerInternalServerErrorException` - raised when the API returns a 500 status code. 294 | * `HetznerActionException` - raised when an action on something yields an error in the JSON response or the status code 295 | is not what was expected. 296 | * `HetznerInvalidArgumentException` - raised when a required argument of the method is not specified correctly. The 297 | exception will detail the failing parameter. 298 | 299 | ### Datacenters 300 | 301 | #### Top level actions 302 | 303 | The datacenter top level action can be retrieved by calling the `datacenters()` method on the `HetznerCloudClient` 304 | instance. 305 | 306 | ##### Get all datacenters 307 | 308 | To retrieve all of the datacenters available on the Hetzner Cloud service, simply call the `get_all()` method, passing 309 | in no parameters. 310 | 311 | *NOTE: This method returns a generator, so if you wish to get all of the results instantly, you should encapsulate the 312 | call within the `list()` function* 313 | 314 | ```python 315 | all_dcs_generator = client.datacenters().get_all() 316 | for dc in all_dcs_generator: 317 | print(dc.id) 318 | 319 | all_dcs_list = list(client.datacenters().get_all()) 320 | print(all_dcs_list) 321 | ``` 322 | 323 | ##### Get all datacenters by name 324 | 325 | To get all datacenters filtered by a name, call the `get_all()` method with the name parameter populated. 326 | 327 | ```python 328 | all_dcs = list(client.datacenters().get_all(name="fsn1-dc8")) 329 | print(all_dcs) 330 | ``` 331 | 332 | ##### Get datacenter by id 333 | 334 | To get a datacenter by id, simply call the `get()` method on the datacenter action, passing in the id of the datacenter 335 | you wish to get information for. 336 | 337 | ```python 338 | datacenter = client.datacenters().get(1) 339 | print(datacenter.name) 340 | ``` 341 | 342 | ### Floating IPs 343 | 344 | #### Floating IPs top level actions 345 | 346 | ##### Create floating IP 347 | 348 | To create a floating IP, simply call the `create()` method with the parameters detailed below. 349 | 350 | *NOTE: If you do not specify `server`, then you must specify `home_location`, or vice versa.* 351 | 352 | ```python 353 | new_floating_ip = client.floating_ips().create(type=FLOATING_IP_TYPE_IPv4, 354 | server=42, 355 | description="My new floating IP") 356 | 357 | " or... 358 | 359 | new_floating_ip = client.floating_ips().create(type=FLOATING_IP_TYPE_IPv4, 360 | home_location="fep1", 361 | description="My new floating IP") 362 | ``` 363 | 364 | ##### Get all floating IPs 365 | 366 | ```python 367 | floating_ips = client.floating_ips().get_all() 368 | for ip in floating_ips: 369 | print(ip.id) 370 | ``` 371 | 372 | ##### Get floating IP by id 373 | 374 | ```python 375 | floating_ip = client.floating_ips().get(1) 376 | print(floating_ip.id) 377 | ``` 378 | 379 | #### Floating IPs modifier actions 380 | 381 | ##### Assign floating IP to server 382 | 383 | ```python 384 | floating_ip = client.floating_ips().get(1) 385 | action = floating_ip.assign_to_server(2) 386 | action.wait_until_status_is(ACTION_STATUS_RUNNING) 387 | ``` 388 | 389 | ##### Change floating IP description 390 | 391 | ```python 392 | floating_ip = client.floating_ips().get(1) 393 | action = floating_ip.change_description("My new floating IP v2") 394 | ``` 395 | 396 | ##### Change floating IP reverse DNS entry 397 | 398 | ```python 399 | floating_ip = client.floating_ips().get(1) 400 | action = floating_ip.change_reverse_dns_entry("192.168.1.1", "www.google.com") 401 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 402 | ``` 403 | 404 | ##### Delete floating IP 405 | 406 | ```python 407 | floating_ip = client.floating_ips().get(1) 408 | action = floating_ip.delete() 409 | ``` 410 | 411 | ##### Unassign floating IP from server 412 | 413 | ```python 414 | floating_ip = client.floating_ips().get(1) 415 | action = floating_ip.unassign_from_server() 416 | ``` 417 | 418 | ### Images 419 | 420 | #### Images top level actions 421 | 422 | ##### Get all images 423 | 424 | To retrieve all of the images available on the Hetzner Cloud service, simply call the `get_all()` method, passing 425 | in no parameters. 426 | 427 | There are also a number of parameters on this method that allow you to filter and sort images. 428 | 429 | ```python 430 | images = client.images().get_all(sort=SORT_BY_ID_ASC) 431 | for image in images: 432 | print(image.id) 433 | ``` 434 | 435 | ##### Get image by id 436 | 437 | To get an image by its id, simply call the `get()` method, passing in the id of the image you wish to retrieve. 438 | 439 | ```python 440 | image = client.images().get(1) 441 | print(image.id) 442 | ``` 443 | 444 | #### Images modifier actions 445 | 446 | ##### Update image 447 | 448 | To update an image's description or type, call the `update()` method with the description of the image and/or the type 449 | of the image, should you wish to update them. Both parameters are optional. 450 | 451 | ```python 452 | image = client.images().get(1) 453 | image.update(description="my description", type=IMAGE_TYPE_SNAPSHOT) 454 | ``` 455 | 456 | ##### Delete image 457 | 458 | To delete an image, call the `delete()` method. NOTE: Only images of type 'snapshot' or 'backup' can be deleted (so, 459 | you cannot delete the images provided by Hetzner!). 460 | 461 | ```python 462 | image = list(client.images().get_all(name="my-first-image"))[0] 463 | image.delete() 464 | ``` 465 | 466 | ### ISOs 467 | 468 | #### Top level actions 469 | 470 | The iso top level action can be retrieved by calling the `isos()` method on the `HetznerCloudClient` 471 | instance. 472 | 473 | ##### Get all ISOs 474 | 475 | To retrieve all of the isos available on the Hetzner Cloud service, simply call the `get_all()` method, passing 476 | in no parameters. 477 | 478 | *NOTE: This method returns a generator, so if you wish to get all of the results instantly, you should encapsulate the 479 | call within the `list()` function* 480 | 481 | ```python 482 | isos = client.isos().get_all() 483 | for l in isos: 484 | print(l.id) 485 | 486 | isos = list(client.isos().get_all()) 487 | print(isos) 488 | ``` 489 | 490 | ##### Get all ISOs by name 491 | 492 | To get all isos filtered by a name, call the `get_all()` method with the name parameter populated. 493 | 494 | ```python 495 | isos = list(client.isos().get_all(name="virtio-win-0.1.141.iso")) 496 | print(isos) 497 | ``` 498 | 499 | ##### Get ISO by id 500 | 501 | To get an iso by id, simply call the `get()` method on the datacenter action, passing in the id of the iso 502 | you wish to get information for. 503 | 504 | ```python 505 | iso = client.isos().get(1) 506 | print(iso.name) 507 | ``` 508 | 509 | ### Locations 510 | 511 | #### Top level actions 512 | 513 | The location top level action can be retrieved by calling the `locations()` method on the `HetznerCloudClient` 514 | instance. 515 | 516 | ##### Get all locations 517 | 518 | To retrieve all of the locations available on the Hetzner Cloud service, simply call the `get_all()` method, passing 519 | in no parameters. 520 | 521 | *NOTE: This method returns a generator, so if you wish to get all of the results instantly, you should encapsulate the 522 | call within the `list()` function* 523 | 524 | ```python 525 | all_locs_generator = client.locations().get_all() 526 | for l in all_locs_generator: 527 | print(l.id) 528 | 529 | all_locs_list = list(client.locations().get_all()) 530 | print(all_locs_list) 531 | ``` 532 | 533 | ##### Get all locations by name 534 | 535 | To get all locations filtered by a name, call the `get_all()` method with the name parameter populated. 536 | 537 | ```python 538 | all_locs = list(client.locations().get_all(name="fsn1")) 539 | print(all_locs) 540 | ``` 541 | 542 | ##### Get location by id 543 | 544 | To get a location by id, simply call the `get()` method on the datacenter action, passing in the id of the location 545 | you wish to get information for. 546 | 547 | ```python 548 | location = client.locations().get(1) 549 | print(location.name) 550 | ``` 551 | 552 | 553 | ### Servers 554 | 555 | The servers top level action is accessible through the `client.servers()` method. You must use one of the methods in 556 | the object returned by this top level action in order to modify the state of individual servers. 557 | 558 | #### Server top level actions 559 | 560 | ##### Get all servers 561 | 562 | All servers associated with the API key you provided can be retrieved by calling the `get_all()` top level action method. 563 | 564 | ```python 565 | all_servers = client.servers().get_all() # gets all the servers as a generator 566 | all_servers_list = list(client.servers().get_all()) # gets all the servers as a list 567 | ``` 568 | 569 | ##### Get all servers by name 570 | 571 | By calling the `get_all(name="my-server-name")` method (with the optional `name` parameter entered), you can bring back 572 | the servers that have the name entered. 573 | 574 | ```python 575 | all_servers = client.servers().get_all(name="foo") # gets all the servers as a generator 576 | all_servers_list = list(client.servers().get_all(name="foo")) # gets all the servers as a list 577 | ``` 578 | 579 | ##### Get server by id 580 | 581 | If you know the id of the server you wish to retrieve you can use this method to retrieve that specific server. 582 | 583 | ```python 584 | try: 585 | server = client.servers().get(1) 586 | except HetznerServerNotFoundException: 587 | print("Woops, server not found!") 588 | ``` 589 | 590 | This method throws a `HetznerServerNotFoundException` if the following conditions are satisfied: 591 | 592 | * The id passed into the method is not an integer or is not greater than 0. 593 | * The API returns a 404 indicating that the server could not be found. 594 | 595 | ##### Create server 596 | 597 | To create a server, you can call the `create` top level action method. This method accepts a number of parameters (some 598 | are optional, some aren't). 599 | 600 | ```python 601 | server_a, create_action = client.servers().create(name="my-required-server-name", # REQUIRED 602 | server_type=SERVER_TYPE_1CPU_2GB, # REQUIRED 603 | image=IMAGE_UBUNTU_1604, # REQUIRED 604 | datacenter=DATACENTER_FALKENSTEIN_1, 605 | start_after_create=True, 606 | ssh_keys=["my-ssh-key-1", "my-ssh-key-2"], 607 | user_data='''#cloud-config 608 | packages: 609 | - screen 610 | - git 611 | ''') 612 | server_a.wait_until_status_is(SERVER_STATUS_RUNNING) 613 | ``` 614 | For more details on user_data please see https://cloudinit.readthedocs.io/en/latest/topics/examples.html 615 | 616 | #### Server modifier actions 617 | 618 | Once you have an instance of the server (retrieved by using one of the "Top level actions" above), you are able to 619 | perform different modifier actions on them. 620 | 621 | ##### Attach ISO 622 | 623 | To attach an ISO to the server, call the `attach_iso()` method on the `HetznerCloudServer` object, specifying either the 624 | name or the ID of the ISO you wish to attach to the server (these can be retrieved by using the `isos().get_all()` 625 | method on the client). 626 | 627 | *NOTE: Constants will **not** be provided for the ISOs, as they are too dynamic and likely to change.* 628 | 629 | ```python 630 | server = client.servers().get(1) 631 | iso_action = server.attach_iso("virtio-win-0.1.141.iso") 632 | iso_action.wait_until_status_is(ACTION_STATUS_SUCCESS) 633 | ``` 634 | 635 | ##### Change reverse DNS 636 | 637 | To change the reverse DNS record associated with the server, call the `change_reverse_dns_entry()` method on the 638 | `HetznerCloudServer` object, specifying the IP address you wish to add a record for and the reverse DNS record. 639 | 640 | *NOTE: If you leave the `dns_pointer` parameter as `None`, the reverse DNS record will be reverted back to what Hetzner 641 | set it as when they created your server.* 642 | 643 | ```python 644 | server = client.servers().get(1) 645 | action = server.change_reverse_dns_entry("192.168.1.1", "www.google.com") 646 | ``` 647 | 648 | ##### Change server name 649 | 650 | To change the server name, call the `change_name()` method on the `HetznerCloudServer` object, specifying a valid name 651 | as the first and only parameter. 652 | 653 | *NOTE: This method does not return an action, so it is assumed that the update is processed immediately. You can verify 654 | this assumption by renaming the server, immediately retrieving it by its id and checking the name of the retrieved 655 | server is what you renamed it to.* 656 | 657 | ```python 658 | server = client.servers().get(1) 659 | server.change_name("my-new-server-name") 660 | ``` 661 | 662 | ##### Change server type 663 | 664 | To change the server type (i.e. from a small, to a large instance), call the `change_type()` method on the 665 | `HetznerCloudServer` object, specifying the new server type as the first parameter and whether to resize the 666 | disk as the second parameter. 667 | 668 | *NOTE: Your server needs to be powered off in order for this to work.* 669 | 670 | *NOTE: If you wish to downgrade the server type in the future, make sure you set the `upgrade_disk` parameter to False. 671 | Not doing this will result in an error being thrown should you try to downgrade in the future.* 672 | 673 | ```python 674 | server = client.servers().get(1) 675 | action = server.change_type(SERVER_TYPE_2CPU_4GB, False) 676 | 677 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 678 | ``` 679 | 680 | ##### Delete server 681 | 682 | To delete the server, call the `delete()` method on the `HetznerCloudServer` object. 683 | 684 | ```python 685 | server = client.servers().get(1) 686 | action = server.delete() 687 | 688 | # Wait until the delete action has completed. 689 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 690 | ``` 691 | 692 | ##### Detach ISO 693 | 694 | To detach the ISO from the server (if there is one present), call the `detach_iso()` method on the `HetznerCloudServer` 695 | object. 696 | 697 | *NOTE: Calling this method when no ISO is attached to the server will succeed and not throw an error.* 698 | 699 | ```python 700 | server = client.servers.get(1) 701 | action = server.detach_iso() 702 | 703 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 704 | ``` 705 | 706 | ##### Disable rescue mode 707 | 708 | To disable rescue mode on the server, simply call the `disable_rescue_mode()` on the server object. 709 | 710 | *NOTE*: Although the API documentation says an error will be returned if rescue mode is already disabled, we have found 711 | this is not the case. 712 | 713 | ```python 714 | server = client.servers().get(1) 715 | disable_action = server.disable_rescue_mode() 716 | 717 | disable_action.wait_until_status_is(ACTION_STATUS_SUCCESS) 718 | ``` 719 | 720 | ##### Enable server backups 721 | 722 | To enable server backups, simply call the `enable_backups()` method on the server object with your chosen backup window. 723 | 724 | ```python 725 | server = client.servers().get(1) 726 | action = server.enable_backups(BACKUP_WINDOW_2AM_6AM) 727 | ``` 728 | 729 | This method throws a `HetznerActionException` if the backup window is not one of the valid choices (see: 730 | https://docs.hetzner.cloud/#resources-server-actions-post-11). 731 | 732 | ##### Enable rescue mode 733 | 734 | To enable rescue mode, simply call the `enable_rescue_mode()` method on the server object. You can specify a rescue 735 | image and an array of SSH keys to load into it. 736 | 737 | *NOTE*: If you use the FreeBSD rescue image, you will not be able to load in any SSH keys. 738 | 739 | *NOTE:* If you want to use the rescue mode, you will need to reboot your server. This method will not automatically 740 | do that for you. 741 | 742 | ```python 743 | server = client.servers().get(1) 744 | root_password, enable_action = server.enable_rescue_mode(rescue_type=RESCUE_TYPE_LINUX32, ssh_keys=["my-ssh-key"]) 745 | 746 | enable_action.wait_until_status_is(ACTION_STATUS_SUCCESS) 747 | 748 | print("Your root password for the rescue mode is %s" % root_password) 749 | ``` 750 | 751 | ##### Image server 752 | 753 | To image a server (i.e. create a backup or a snapshot), call the `image()` method on the server object. You can specify 754 | the description of the server and the type of image you are creating. Possible image types are as follows: 755 | 756 | * `backup` - An image that is bound to the server and deleted when the server is. **Only available when server backups 757 | are enabled**. 758 | * `snapshot` - An image created independent of the server and billed seperately. 759 | 760 | ```python 761 | server = client.servers().get(1) 762 | image_id, image_action = server.image("My backup image", type=IMAGE_TYPE_BACKUP) 763 | 764 | image_action.wait_until_status_is(ACTION_STATUS_SUCCESS) 765 | 766 | print("The new backup image identifier is %s" % image_id) 767 | ``` 768 | 769 | ##### Power on 770 | 771 | To power a server on, simply call the `power_on()` method on the server object. 772 | 773 | ##### Power off 774 | 775 | To power a server off, simply call the `power_off()` method on the server object. 776 | 777 | ##### Rebuild from image 778 | 779 | To rebuild a server from an image, simply call the `rebuild_from_image()` method on the server object, passing in the 780 | image id or name you wish to overwrite the server with. 781 | 782 | *NOTE: This method will destroy **all** data on the server* 783 | 784 | ```python 785 | server = client.servers().get(1) 786 | action = server.rebuild_from_image(IMAGE_UBUNTU_1604) 787 | 788 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 789 | ``` 790 | 791 | ##### Reset server 792 | 793 | To reset a server (equivelent to pulling the power cord and plugging it back in), simply call the `reset()` method on 794 | the server object. 795 | 796 | ```python 797 | server = client.servers().get(1) 798 | action = server.reset() 799 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 800 | ``` 801 | 802 | ##### Reset root password 803 | 804 | To reset the password of the Root account on a server, simply call the `reset_root_password()` method on the server 805 | object. 806 | 807 | ```python 808 | server = client.servers().get(1) 809 | root_password, reset_action = server.reset_root_password() 810 | 811 | print("The new root password is %s" % root_password) 812 | ``` 813 | 814 | ##### Shutdown server 815 | 816 | To shutdown a server gracefully, simply call the `shutdown()` method on the server object. 817 | 818 | *NOTE: The OS on the server you are trying to shut down must support ACPI.* 819 | 820 | ```python 821 | server = client.servers().get(1) 822 | server.shutdown() 823 | ``` 824 | 825 | ##### Wait for server status 826 | 827 | You can wait for a server to have a particular status by calling the `wait_until_status_is(desired_status)` method on 828 | the server object. 829 | 830 | This method will loop a set number of times (defined by the `attempts` parameter) and pause each time for one second 831 | (or a timespan defined by modifying the wait_seconds parameter) until the number of attempts is exceeded 832 | or the condition matches. 833 | 834 | This is useful when you want to ensure your server is of a particular state before performing any actions on it. 835 | 836 | ```python 837 | server = client.servers().get(1) 838 | 839 | try: 840 | server.wait_until_status_is(SERVER_STATUS_OFF, attempts=50, wait_seconds=10) 841 | except HetznerWaitAttemptsExceededException: 842 | print("Server status was not updated in 50 attempts") 843 | ``` 844 | 845 | This method throws a `HetznerWaitAttemptsExceededException` should the amount of attempts be exceeded with the condition 846 | still being unmet. 847 | 848 | ### Server size types 849 | 850 | #### Top level actions 851 | 852 | The server types top level action can be retrieved by calling the `server_types()` method on the `HetznerCloudClient` 853 | instance. 854 | 855 | ##### Get all server types 856 | 857 | To retrieve all of the server types available on the Hetzner Cloud service, simply call the `get_all()` method, passing 858 | in no parameters. 859 | 860 | *NOTE: This method returns a generator, so if you wish to get all of the results instantly, you should encapsulate the 861 | call within the `list()` function* 862 | 863 | ```python 864 | types = client.server_types().get_all() 865 | for l in types: 866 | print(l.id) 867 | 868 | types = list(client.server_types().get_all()) 869 | print(types) 870 | ``` 871 | 872 | ##### Get all server types by name 873 | 874 | To get all server types filtered by a name, call the `get_all()` method with the name parameter populated. 875 | 876 | ```python 877 | server_types = list(client.server_types().get_all(name="cx11")) 878 | print(server_types) 879 | ``` 880 | 881 | ##### Get server type by id 882 | 883 | To get a server type by id, simply call the `get()` method on the datacenter action, passing in the id of the server type 884 | you wish to get information for. 885 | 886 | ```python 887 | stype = client.server_types().get(1) 888 | print(stype.name) 889 | ``` 890 | 891 | ### SSH keys 892 | 893 | #### SSH keys top level actions 894 | 895 | ##### Create SSH key 896 | 897 | To create an SSH key, call the `create()` method on the `HetznerCloudSSHKeysAction` with the name and the public key of 898 | the SSH key you wish to add. 899 | 900 | ```python 901 | new_ssh_key = client.ssh_keys().create(name="My new SSH key", public_key="abcdef") 902 | print(new_ssh_key.fingerprint) 903 | ``` 904 | 905 | ##### Get all SSH keys 906 | 907 | To get all SSH keys, call the `get_all()` method. NOTE: This object returned from this method is a generator. 908 | 909 | ```python 910 | ssh_keys = client.ssh_keys().get_all() 911 | for ssh_key in ssh_keys: 912 | print(ssh_key.id) 913 | ``` 914 | 915 | ##### Get all SSH keys by name 916 | 917 | To get all SSH keys by their name, call the `get_all()` method, but pass in the name of the SSH key you wish to search 918 | for. 919 | 920 | ```python 921 | ssh_keys = list(client.ssh_keys().get_all(name="My SSH key")) 922 | for ssh_key in ssh_keys: 923 | print(ssh_key.id) 924 | ``` 925 | 926 | ##### Get SSH key by id 927 | 928 | ```python 929 | ssh_key = client.ssh_keys().get(1) 930 | print(ssh_key.id) 931 | ``` 932 | 933 | #### SSH keys modifier actions 934 | 935 | ##### Delete SSH key 936 | 937 | To delete an SSH key, call the `delete()` method on the SSH key object. 938 | 939 | ```python 940 | ssh_key = client.ssh_keys().get(1) 941 | ssh_key.delete() 942 | ``` 943 | 944 | ##### Update SSH key 945 | 946 | To update an SSH key's name, call the `update()` method on the SSH key object. 947 | 948 | ```python 949 | ssh_key = client.ssh_keys().get(1) 950 | ssh_key.update(name="Foo") 951 | ``` 952 | -------------------------------------------------------------------------------- /hetznercloud/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import HetznerCloudClientConfiguration, HetznerCloudClient 2 | from .constants import * 3 | from .exceptions import * 4 | -------------------------------------------------------------------------------- /hetznercloud/actions.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .constants import ACTION_STATUS_ERROR 4 | from .exceptions import HetznerWaitAttemptsExceededException, HetznerInternalServerErrorException 5 | from .shared import _get_results 6 | 7 | 8 | def _get_action_json(config, id): 9 | status_code, json = _get_results(config, "actions/%s" % id) 10 | return json["action"] 11 | 12 | class HetznerCloudActionsAction(object): 13 | pass 14 | 15 | class HetznerCloudAction(object): 16 | """ 17 | When actions in the API (such as creating a server) are performed, the API will naturally not wait until the request 18 | finishes its requested action (as these things can sometimes take a long time). 19 | 20 | Instead, the cloud API returns an 'action', which can be used to check up on the status of specific tasks. 21 | """ 22 | def __init__(self, config): 23 | self.config = config 24 | self.id = 0 25 | self.command = "" 26 | self.status = "" 27 | self.progress = 0 28 | self.started = "" 29 | self.finished = "" 30 | self.error = { "code": "", "message": "" } 31 | 32 | def wait_until_status_is(self, status, attempts=20, wait_seconds=1): 33 | """ 34 | Sleeps the executing thread (a second each loop) until the status is either what the user requires or the 35 | attempt count is exceeded, in which case an exception is thrown. 36 | 37 | :param status: The status the action needs to be. 38 | :param attempts: The number of attempts to query the action's status. 39 | :param wait_seconds: The number of seconds to wait for between each attempt. 40 | :return: An exception, unless the status matches the status parameter. 41 | """ 42 | if self.status == status: 43 | return 44 | 45 | for i in range(0, attempts): 46 | action_status = _get_action_json(self.config, self.id) 47 | if action_status["status"] == status: 48 | self.status = action_status 49 | return 50 | 51 | if action_status["status"] == ACTION_STATUS_ERROR: 52 | raise HetznerInternalServerErrorException(action_status["error"]) 53 | 54 | time.sleep(wait_seconds) 55 | 56 | raise HetznerWaitAttemptsExceededException() 57 | 58 | @staticmethod 59 | def _load_from_json(configuration, json): 60 | action = HetznerCloudAction(configuration) 61 | 62 | action.id = json["id"] 63 | action.command = json["command"] 64 | action.status = json["status"] 65 | action.progress = json["progress"] 66 | action.started = json["started"] 67 | action.finished = json["finished"] 68 | action.error = json["error"] 69 | 70 | return action -------------------------------------------------------------------------------- /hetznercloud/client.py: -------------------------------------------------------------------------------- 1 | from .floating_ips import HetznerCloudFloatingIpAction 2 | from .ssh_keys import HetznerCloudSSHKeysAction 3 | from .images import HetznerCloudImagesAction 4 | from .datacenters import HetznerCloudDatacentersAction 5 | from .exceptions import HetznerConfigurationException 6 | from .isos import HetznerCloudIsosAction 7 | from .locations import HetznerCloudLocationsAction 8 | from .server_types import HetznerCloudServerTypesAction 9 | from .servers import HetznerCloudServersAction 10 | 11 | 12 | class HetznerCloudClientConfiguration(object): 13 | def __init__(self): 14 | self.api_key = "" 15 | self.api_version = 1 16 | 17 | def with_api_key(self, key): 18 | self.api_key = key 19 | return self 20 | 21 | def with_api_version(self, version): 22 | """ 23 | Modifies the API version associated with this configuration object. Currently, there is only one API version 24 | available (1), so using this method to pick a different version will result in a configuration exception being 25 | raised when this object is passed into the HetznerCloudClient constructor. 26 | 27 | :rtype: object 28 | :param version: The version of the client to use. 29 | :return: The current instance of the cloud configuration object to allow fluent chaining. 30 | """ 31 | self.api_version = version 32 | return self 33 | 34 | 35 | class HetznerCloudClient(object): 36 | def __init__(self, configuration): 37 | if not isinstance(configuration, HetznerCloudClientConfiguration): 38 | raise HetznerConfigurationException("Invalid configuration type.") 39 | 40 | if not configuration.api_key: 41 | raise HetznerConfigurationException("Invalid API key.") 42 | 43 | if not isinstance(configuration.api_version, int) or configuration.api_version != 1: 44 | raise HetznerConfigurationException("The requested API version is not yet supported.") 45 | 46 | self.configuration = configuration 47 | 48 | # alias for datacentres method 49 | self.datacenters = self.datacentres 50 | 51 | def datacentres(self): 52 | return HetznerCloudDatacentersAction(self.configuration) 53 | 54 | def floating_ips(self): 55 | return HetznerCloudFloatingIpAction(self.configuration) 56 | 57 | def images(self): 58 | return HetznerCloudImagesAction(self.configuration) 59 | 60 | def isos(self): 61 | return HetznerCloudIsosAction(self.configuration) 62 | 63 | def locations(self): 64 | return HetznerCloudLocationsAction(self.configuration) 65 | 66 | def server_types(self): 67 | return HetznerCloudServerTypesAction(self.configuration) 68 | 69 | def servers(self): 70 | return HetznerCloudServersAction(self.configuration) 71 | 72 | def ssh_keys(self): 73 | return HetznerCloudSSHKeysAction(self.configuration) 74 | -------------------------------------------------------------------------------- /hetznercloud/constants.py: -------------------------------------------------------------------------------- 1 | SERVER_STATUS_RUNNING = "running" 2 | SERVER_STATUS_INITIALIZING = "initializing" 3 | SERVER_STATUS_STARTING = "starting" 4 | SERVER_STATUS_STOPPING = "stopping" 5 | SERVER_STATUS_OFF = "off" 6 | SERVER_STATUS_DELETING = "deleting" 7 | SERVER_STATUS_MIGRATING = "migrating" 8 | SERVER_STATUS_REBUILDING = "rebuilding" 9 | SERVER_STATUS_UNKNOWN = "unknown" 10 | 11 | ACTION_STATUS_SUCCESS = "success" 12 | ACTION_STATUS_RUNNING = "running" 13 | ACTION_STATUS_ERROR = "error" 14 | 15 | RESCUE_TYPE_FREEBSD = "freebsd64" 16 | RESCUE_TYPE_LINUX = "linux64" 17 | RESCUE_TYPE_LINUX32 = "linux32" 18 | 19 | SERVER_TYPE_1CPU_2GB = "cx11" 20 | SERVER_TYPE_1CPU_2GB_CEPH = "cx11-ceph" 21 | SERVER_TYPE_2CPU_4GB = "cx21" 22 | SERVER_TYPE_2CPU_4GB_CEPH = "cx21-ceph" 23 | SERVER_TYPE_2CPU_8GB = "cx31" 24 | SERVER_TYPE_2CPU_8GB_CEPH = "cx31-ceph" 25 | SERVER_TYPE_4CPU_16GB = "cx41" 26 | SERVER_TYPE_4CPU_16GB_CEPH = "cx41-ceph" 27 | SERVER_TYPE_8CPU_32GB = "cx51" 28 | SERVER_TYPE_8CPU_32GB_CEPH = "cx51-ceph" 29 | 30 | SERVER_TYPE_2CPU_8GB_DVCPU = "ccx11" 31 | SERVER_TYPE_4CPU_16GB_DVCPU = "ccx21" 32 | SERVER_TYPE_8CPU_32GB_DVCPU = "ccx31" 33 | SERVER_TYPE_16CPU_64GB_DVCPU = "ccx41" 34 | SERVER_TYPE_32CPU_128GB_DVCPU = "ccx51" 35 | 36 | IMAGE_UBUNTU_1604 = "ubuntu-16.04" 37 | IMAGE_UBUNTU_1804 = "ubuntu-18.04" 38 | IMAGE_DEBIAN_9 = "debian-9" 39 | IMAGE_CENTOS_7 = "centos-7" 40 | IMAGE_FEDORA_27 = "fedora-27" 41 | IMAGE_FEDORA_28 = "fedora-28" 42 | 43 | IMAGE_TYPE_BACKUP = "backup" 44 | IMAGE_TYPE_SNAPSHOT = "snapshot" 45 | 46 | DATACENTER_FALKENSTEIN_1 = "fsn1-dc8" 47 | DATACENTER_NUREMBERG_1 = "nbg1-dc3" 48 | DATACENTER_HELSINKI_1 = "hel1-dc2" 49 | 50 | BACKUP_WINDOW_10PM_2AM = "22-02" 51 | BACKUP_WINDOW_2AM_6AM = "02-06" 52 | BACKUP_WINDOW_6AM_10AM = "06-10" 53 | BACKUP_WINDOW_10AM_2PM = "10-14" 54 | BACKUP_WINDOW_2PM_6PM = "14-18" 55 | BACKUP_WINDOW_6PM_10PM = "18-22" 56 | 57 | SORT_BY_ID_ASC = "id:asc" 58 | SORT_BY_ID_DESC = "id:desc" 59 | SORT_BY_NAME_ASC = "name:asc" 60 | SORT_BY_NAME_DESC = "name:desc" 61 | SORT_BY_CREATED_ASC = "created:asc" 62 | SORT_BY_CREATED_DESC = "created:desc" 63 | 64 | FLOATING_IP_TYPE_IPv4 = "ipv4" 65 | FLOATING_IP_TYPE_IPv6 = "ipv6" -------------------------------------------------------------------------------- /hetznercloud/datacenters.py: -------------------------------------------------------------------------------- 1 | from .exceptions import HetznerActionException 2 | from .locations import HetznerCloudLocation 3 | from .shared import _get_results 4 | 5 | 6 | class HetznerCloudDatacentersAction(object): 7 | def __init__(self, config): 8 | self._config = config 9 | 10 | def get_all(self, name=None): 11 | status_code, results = _get_results(self._config, "datacenters", 12 | url_params={"name": name} if name is not None else None) 13 | if status_code != 200: 14 | raise HetznerActionException(results) 15 | 16 | for result in results["datacenters"]: 17 | yield HetznerCloudDatacenter._load_from_json(result) 18 | 19 | def get(self, id): 20 | status_code, results = _get_results(self._config, "datacenters/%s" % id) 21 | if status_code != 200: 22 | raise HetznerActionException(results) 23 | 24 | return HetznerCloudDatacenter._load_from_json(results["datacenter"]) 25 | 26 | 27 | class HetznerCloudDatacenter(object): 28 | def __init__(self): 29 | self.id = 0 30 | self.name = "" 31 | self.description = "" 32 | self.location = None 33 | self.supported_server_types = [] 34 | self.available_server_types = [] 35 | 36 | @staticmethod 37 | def _load_from_json(json): 38 | dc = HetznerCloudDatacenter() 39 | 40 | dc.id = int(json["id"]) 41 | dc.name = json["name"] 42 | dc.description = json["description"] 43 | dc.location = HetznerCloudLocation._load_from_json(json["location"]) 44 | dc.supported_server_types = json["server_types"]["supported"] 45 | dc.available_server_types = json["server_types"]["available"] 46 | 47 | return dc 48 | -------------------------------------------------------------------------------- /hetznercloud/exceptions.py: -------------------------------------------------------------------------------- 1 | class HetznerConfigurationException(Exception): 2 | def __init__(self, reason): 3 | super().__init__("Your Hetzner Cloud configuration is incorrect: %s" % reason) 4 | 5 | 6 | class HetznerServerNotFoundException(Exception): 7 | pass 8 | 9 | 10 | class HetznerAuthenticationException(Exception): 11 | def __init__(self): 12 | super().__init__("Authenticated failed. Is your API key correct?") 13 | 14 | 15 | class HetznerInternalServerErrorException(Exception): 16 | def __init__(self, hetzner_message): 17 | super().__init__("The Hetzner Cloud API is currently unavailable: %s" % hetzner_message) 18 | 19 | 20 | class HetznerInvalidArgumentException(Exception): 21 | def __init__(self, argument, message=None): 22 | if message is not None: 23 | super().__init__("An invalid argument was (or was not) entered: %s, %s." % (argument, message)) 24 | else: 25 | super().__init__("An invalid argument was (or was not) entered: %s." % argument) 26 | 27 | 28 | class HetznerActionException(Exception): 29 | def __init__(self, error_code=None): 30 | if error_code is not None: 31 | super().__init__("Failed to perform the requested action: %s" % error_code) 32 | else: 33 | super().__init__("Failed to perform the requested action.") 34 | 35 | 36 | class HetznerWaitAttemptsExceededException(Exception): 37 | pass 38 | 39 | class HetznerRateLimitExceeded(Exception): 40 | pass 41 | -------------------------------------------------------------------------------- /hetznercloud/floating_ips.py: -------------------------------------------------------------------------------- 1 | from hetznercloud.actions import HetznerCloudAction 2 | from .exceptions import HetznerInvalidArgumentException, HetznerActionException 3 | from .shared import _get_results 4 | 5 | 6 | class HetznerCloudFloatingIpAction(object): 7 | def __init__(self, config): 8 | self._config = config 9 | 10 | def create(self, type, home_location=None, server=None, description=None): 11 | if home_location is None and server is None: 12 | raise HetznerInvalidArgumentException("home_location_id and server") 13 | 14 | body = {"type": type} 15 | if home_location is not None: 16 | body["home_location"] = home_location 17 | if server is not None: 18 | body["server"] = server 19 | if description is not None: 20 | body["description"] = description 21 | 22 | status_code, results = _get_results(self._config, "floating_ips", method="POST", body=body) 23 | if status_code != 201: 24 | raise HetznerActionException(results) 25 | 26 | return HetznerCloudFloatingIp._load_from_json(self._config, results["floating_ip"]) 27 | 28 | def get_all(self): 29 | status_code, results = _get_results(self._config, "floating_ips") 30 | if status_code != 200: 31 | raise HetznerActionException(results) 32 | 33 | for result in results["floating_ips"]: 34 | yield HetznerCloudFloatingIp._load_from_json(self._config, result) 35 | 36 | def get(self, id): 37 | status_code, result = _get_results(self._config, "floating_ips/%s" % id) 38 | if status_code != 200: 39 | raise HetznerActionException(result) 40 | 41 | return HetznerCloudFloatingIp._load_from_json(self._config, result["floating_ip"]) 42 | 43 | 44 | class HetznerCloudFloatingIp(object): 45 | def __init__(self, config): 46 | self._config = config 47 | self.id = 0 48 | self.ip = "" 49 | self.description = "" 50 | self.type = "" 51 | self.server = 0 52 | self.ptr_ips = [] 53 | self.ptr_dns_ptrs = [] 54 | self.location_id = 0 55 | self.blocked = False 56 | 57 | def assign_to_server(self, server_id): 58 | if not server_id: 59 | raise HetznerInvalidArgumentException("server_id") 60 | 61 | status_code, result = _get_results(self._config, "floating_ips/%s/actions/assign" % self.id, method="POST", 62 | body={"server": server_id}) 63 | if status_code != 201: 64 | raise HetznerActionException(result) 65 | 66 | self.server = server_id 67 | 68 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 69 | 70 | def change_description(self, new_description): 71 | status_code, result = _get_results(self._config, "floating_ips/%s" % self.id, method="PUT", 72 | body={"description": new_description}) 73 | if status_code != 200: 74 | raise HetznerActionException(result) 75 | 76 | self.description = new_description 77 | 78 | def change_reverse_dns_entry(self, ip, dns_ptr=None): 79 | if not ip: 80 | raise HetznerInvalidArgumentException("ip") 81 | 82 | status_code, result = _get_results(self._config, "floating_ips/%s/actions/change_dns_ptr" % self.id, 83 | method="POST", body={"ip": ip, "dns_ptr": dns_ptr}) 84 | if status_code != 201: 85 | raise HetznerActionException(result) 86 | 87 | self.ptr_ips = [ip] 88 | self.ptr_dns_ptrs = [dns_ptr] 89 | 90 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 91 | 92 | def delete(self): 93 | status_code, result = _get_results(self._config, "floating_ips/%s" % self.id, method="DELETE") 94 | if status_code != 204: 95 | raise HetznerActionException(result) 96 | 97 | def unassign_from_server(self): 98 | status_code, result = _get_results(self._config, "floating_ips/%s/actions/unassign" % self.id, method="POST") 99 | if status_code != 201: 100 | raise HetznerActionException(result) 101 | 102 | self.server = 0 103 | 104 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 105 | 106 | @staticmethod 107 | def _load_from_json(config, json): 108 | float_ip = HetznerCloudFloatingIp(config) 109 | 110 | float_ip.id = int(json["id"]) 111 | float_ip.description = json["description"] 112 | float_ip.ip = json["ip"] 113 | float_ip.type = json["type"] 114 | float_ip.server = int(json["server"]) if json["server"] is not None else 0 115 | float_ip.ptr_ips = [entry["ip"] for entry in json["dns_ptr"]] 116 | float_ip.ptr_dns_ptrs = [entry["dns_ptr"] for entry in json["dns_ptr"]] 117 | float_ip.location_id = int(json["home_location"]["id"]) 118 | float_ip.blocked = bool(json["blocked"]) 119 | 120 | return float_ip 121 | -------------------------------------------------------------------------------- /hetznercloud/images.py: -------------------------------------------------------------------------------- 1 | from .constants import IMAGE_TYPE_SNAPSHOT 2 | from .exceptions import HetznerActionException 3 | from .shared import _get_results 4 | 5 | 6 | class HetznerCloudImagesAction(object): 7 | def __init__(self, config): 8 | self._config = config 9 | 10 | def get_all(self, sort=None, type=None, bound_to=None, name=None): 11 | url_params = {} 12 | if sort is not None: 13 | url_params["sort"] = sort 14 | if type is not None: 15 | url_params["type"] = type 16 | if bound_to is not None: 17 | url_params["bound_to"] = bound_to 18 | if name is not None: 19 | url_params["name"] = name 20 | 21 | status_code, results = _get_results(self._config, "images?per_page=100", url_params=url_params) 22 | if status_code != 200: 23 | raise HetznerActionException(results) 24 | 25 | for result in results["images"]: 26 | yield HetznerCloudImage._load_from_json(self._config, result) 27 | 28 | def get(self, id): 29 | status_code, results = _get_results(self._config, "images/%s" % id) 30 | if status_code != 200: 31 | raise HetznerActionException(results) 32 | 33 | return HetznerCloudImage._load_from_json(self._config, results["image"]) 34 | 35 | 36 | class HetznerCloudImage(object): 37 | def __init__(self, config): 38 | self._config = config 39 | self.id = 0 40 | self.type = "" 41 | self.status = "" 42 | self.name = "" 43 | self.description = "" 44 | self.image_size = 0 45 | self.disk_size = 0 46 | self.created_from_id = 0 47 | self.created_from_name = "" 48 | self.bound_to = "" 49 | self.os_flavor = "" 50 | self.os_version = "" 51 | self.rapid_deploy = False 52 | 53 | def update(self, description=None, type=None): 54 | body = {} 55 | if description is not None: 56 | body["description"] = description 57 | if type is not None: 58 | body["type"] = type 59 | 60 | status_code, result = _get_results(self._config, "images/%s" % self.id, method="PUT", body=body) 61 | if status_code != 200: 62 | raise HetznerActionException(result) 63 | 64 | self.description = description 65 | self.type = type 66 | 67 | def delete(self): 68 | status_code, result = _get_results(self._config, "images/%s" % self.id, method="DELETE") 69 | if status_code != 204: 70 | raise HetznerActionException(result) 71 | 72 | @staticmethod 73 | def _load_from_json(config, json): 74 | image = HetznerCloudImage(config) 75 | 76 | image.id = int(json["id"]) 77 | image.type = json["type"] 78 | image.status = json["status"] 79 | image.name = json["name"] 80 | image.description = json["description"] 81 | image.image_size = float(json["image_size"]) if json["image_size"] is not None else None 82 | image.disk_size = float(json["disk_size"]) if json["disk_size"] is not None else None 83 | image.created_from_id = int(json["created_from"]["id"]) if json["created_from"] is not None else None 84 | image.created_from_name = json["created_from"]["name"] if json["created_from"] is not None else None 85 | image.bound_to = int(json["bound_to"]) if json["bound_to"] is not None else None 86 | image.os_flavor = json["os_flavor"] 87 | image.os_version = json["os_version"] 88 | image.rapid_deploy = bool(json["rapid_deploy"]) 89 | 90 | return image 91 | -------------------------------------------------------------------------------- /hetznercloud/isos.py: -------------------------------------------------------------------------------- 1 | from .exceptions import HetznerActionException 2 | from .shared import _get_results 3 | 4 | 5 | class HetznerCloudIsosAction(object): 6 | def __init__(self, config): 7 | self._config = config 8 | 9 | def get_all(self, name=None): 10 | status_code, results = _get_results(self._config, "isos?per_page=100", 11 | url_params={"name": name} if name is not None else None) 12 | if status_code != 200: 13 | raise HetznerActionException(results) 14 | 15 | for result in results["isos"]: 16 | yield HetznerCloudIso._load_from_json(result) 17 | 18 | def get(self, id): 19 | status_code, results = _get_results(self._config, "isos/%s" % id) 20 | if status_code != 200: 21 | raise HetznerActionException(results) 22 | 23 | return HetznerCloudIso._load_from_json(results["iso"]) 24 | 25 | 26 | class HetznerCloudIso(object): 27 | def __init__(self): 28 | self.id = 0 29 | self.name = "" 30 | self.description = "" 31 | self.type = "" 32 | 33 | @staticmethod 34 | def _load_from_json(json): 35 | iso = HetznerCloudIso() 36 | 37 | iso.id = int(json["id"]) 38 | iso.name = json["name"] 39 | iso.description = json["description"] 40 | iso.type = json["type"] 41 | 42 | return iso 43 | -------------------------------------------------------------------------------- /hetznercloud/locations.py: -------------------------------------------------------------------------------- 1 | from .exceptions import HetznerActionException 2 | from .shared import _get_results 3 | 4 | 5 | class HetznerCloudLocationsAction(object): 6 | def __init__(self, config): 7 | self._config = config 8 | 9 | def get_all(self, name=None): 10 | status_code, results = _get_results(self._config, "locations", 11 | url_params={"name": name} if name is not None else None) 12 | if status_code != 200: 13 | raise HetznerActionException(results) 14 | 15 | for result in results["locations"]: 16 | yield HetznerCloudLocation._load_from_json(result) 17 | 18 | def get(self, id): 19 | status_code, results = _get_results(self._config, "locations/%s" % id, method="GET") 20 | if status_code != 200: 21 | raise HetznerActionException(results) 22 | 23 | return HetznerCloudLocation._load_from_json(results["location"]) 24 | 25 | 26 | class HetznerCloudLocation(object): 27 | def __init__(self): 28 | self.id = 0 29 | self.name = "" 30 | self.description = "" 31 | self.country = "" 32 | self.city = "" 33 | self.latitude = 0.0 34 | self.longitude = 0.0 35 | 36 | @staticmethod 37 | def _load_from_json(json): 38 | location = HetznerCloudLocation() 39 | 40 | location.id = int(json["id"]) 41 | location.name = json["name"] 42 | location.description = json["description"] 43 | location.country = json["country"] 44 | location.city = json["city"] 45 | location.latitude = float(json["latitude"]) 46 | location.longitude = float(json["longitude"]) 47 | 48 | return location 49 | -------------------------------------------------------------------------------- /hetznercloud/server_types.py: -------------------------------------------------------------------------------- 1 | from .exceptions import HetznerActionException 2 | from .shared import _get_results 3 | 4 | 5 | class HetznerCloudServerTypesAction(object): 6 | def __init__(self, config): 7 | self._config = config 8 | 9 | def get_all(self, name=None): 10 | status_code, results = _get_results(self._config, "server_types", 11 | url_params={"name": name} if name is not None else None) 12 | if status_code != 200: 13 | raise HetznerActionException(results) 14 | 15 | for result in results["server_types"]: 16 | yield HetznerCloudServerType._load_from_json(result) 17 | 18 | def get(self, id): 19 | status_code, results = _get_results(self._config, "server_types/%s" % id) 20 | if status_code != 200: 21 | raise HetznerActionException(results) 22 | 23 | return HetznerCloudServerType._load_from_json(results["server_type"]) 24 | 25 | 26 | class HetznerCloudServerType(object): 27 | def __init__(self): 28 | self.id = 0 29 | self.name = "" 30 | self.description = "" 31 | self.cores = 1 32 | self.memory = 1 33 | self.disk = 1 34 | self.storage_type = "" 35 | 36 | @staticmethod 37 | def _load_from_json(json): 38 | server_type = HetznerCloudServerType() 39 | 40 | server_type.id = json["id"] 41 | server_type.name = json["name"] 42 | server_type.description = json["description"] 43 | server_type.cores = int(json["cores"]) 44 | server_type.memory = int(json["memory"]) 45 | server_type.disk = int(json["disk"]) 46 | server_type.storage_type = json["storage_type"] 47 | 48 | return server_type -------------------------------------------------------------------------------- /hetznercloud/servers.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .actions import HetznerCloudAction 4 | from .constants import RESCUE_TYPE_LINUX, RESCUE_TYPE_FREEBSD, BACKUP_WINDOW_2AM_6AM, SERVER_STATUS_RUNNING, \ 5 | SERVER_STATUS_OFF 6 | from .exceptions import HetznerServerNotFoundException, HetznerInvalidArgumentException, HetznerActionException, \ 7 | HetznerWaitAttemptsExceededException 8 | from .shared import _get_results 9 | 10 | 11 | def _get_server_json(config, server_id): 12 | status_code, result = _get_results(config, "servers/%s" % server_id) 13 | if status_code == 404: 14 | raise HetznerServerNotFoundException() 15 | 16 | if not "server" in result: 17 | raise HetznerActionException(result) 18 | 19 | return result["server"] 20 | 21 | 22 | class HetznerCloudServersAction(object): 23 | def __init__(self, config): 24 | self._config = config 25 | 26 | def create(self, name, server_type, image, datacenter=None, start_after_create=True, ssh_keys=[], user_data=None, location=None): 27 | if not name or not server_type or not image: 28 | raise HetznerInvalidArgumentException("name" if not name 29 | else "server_type" if not server_type 30 | else "image" if not image 31 | else "") 32 | 33 | create_params = { 34 | "name": name, 35 | "server_type": server_type, 36 | "image": image, 37 | "start_after_create": start_after_create 38 | } 39 | 40 | # Giving only `location` instead of `datacenter` is useful if you 41 | # want Hetzner to pick a data center within a given location for you. 42 | if location is not None: 43 | create_params["location"] = location 44 | if datacenter is not None: 45 | create_params["datacenter"] = datacenter 46 | if ssh_keys is not None and len(ssh_keys) > 0: 47 | create_params["ssh_keys"] = ssh_keys 48 | if user_data is not None: 49 | create_params["user_data"] = user_data 50 | 51 | status_code, result = _get_results(self._config, "servers", body=create_params, method="POST") 52 | if status_code != 201 or result is None or ("error" in result and result["error"] is not None): 53 | raise HetznerActionException(result) 54 | 55 | return HetznerCloudServer._load_from_json(self._config, result["server"], result["root_password"]), \ 56 | HetznerCloudAction._load_from_json(self._config, result["action"]) 57 | 58 | def get(self, server_id): 59 | if not isinstance(server_id, int) or server_id == 0: 60 | raise HetznerServerNotFoundException() 61 | 62 | return HetznerCloudServer._load_from_json(self._config, _get_server_json(self._config, server_id)) 63 | 64 | def get_all(self, name=None): 65 | status_code, results = _get_results(self._config, "servers", {"name": name} if name is not None else None) 66 | if status_code != 200: 67 | raise HetznerActionException(results) 68 | 69 | for result in results["servers"]: 70 | yield HetznerCloudServer._load_from_json(self._config, result) 71 | 72 | 73 | class HetznerCloudServer(object): 74 | def __init__(self, config): 75 | self._config = config 76 | self.id = 0 77 | self.name = "" 78 | self.status = "" 79 | self.created = None 80 | self.public_net_ipv4 = "" 81 | self.public_net_ipv6 = "" 82 | self.server_type = "" 83 | self.datacenter_id = 0 84 | self.image_id = "" 85 | self.iso = "" 86 | self.rescue_enabled = False 87 | self.locked = False 88 | self.backup_window = "" 89 | self.outgoing_traffic = 0 90 | self.ingoing_traffic = 0 91 | self.included_traffic = 0 92 | self.root_password = "" 93 | 94 | def attach_iso(self, iso): 95 | if not iso: 96 | raise HetznerInvalidArgumentException("iso") 97 | 98 | status_code, result = _get_results(self._config, "servers/%s/actions/attach_iso" % self.id, method="POST", 99 | body={"iso": iso}) 100 | if status_code != 201: 101 | raise HetznerActionException(result) 102 | 103 | self.iso = iso 104 | 105 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 106 | 107 | def change_name(self, new_name): 108 | if not new_name: 109 | raise HetznerInvalidArgumentException("new_name") 110 | 111 | body = {"name": new_name} 112 | status_code, result = _get_results(self._config, "servers/%s" % self.id, method="PUT", body=body) 113 | if status_code != 200: 114 | raise HetznerActionException(result) 115 | 116 | self.name = new_name 117 | 118 | def change_reverse_dns_entry(self, ip, dns_pointer=None): 119 | if not ip: 120 | raise HetznerInvalidArgumentException("ip") 121 | 122 | status_code, result = _get_results(self._config, "servers/%s/actions/change_dns_ptr" % self.id, method="POST", 123 | body={"ip": ip, "dns_ptr": dns_pointer}) 124 | if status_code != 201: 125 | raise HetznerActionException(result) 126 | 127 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 128 | 129 | def change_type(self, new_instance_type, upgrade_disk=True): 130 | if not new_instance_type: 131 | raise HetznerInvalidArgumentException("new_instance_type") 132 | 133 | status_code, result = _get_results(self._config, "servers/%s/actions/change_type" % self.id, method="POST", 134 | body={"server_type": new_instance_type, "upgrade_disk": upgrade_disk}) 135 | if status_code != 201: 136 | raise HetznerActionException(result) 137 | 138 | self.server_type = new_instance_type 139 | 140 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 141 | 142 | def delete(self): 143 | status_code, result = _get_results(self._config, "servers/%s" % self.id, method="DELETE") 144 | if status_code != 200: 145 | raise HetznerActionException(result) 146 | 147 | self.iso = "" 148 | 149 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 150 | 151 | def detach_iso(self): 152 | status_code, result = _get_results(self._config, "servers/%s/actions/detach_iso", method="POST") 153 | if status_code != 201: 154 | return HetznerActionException(result) 155 | 156 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 157 | 158 | def disable_rescue_mode(self): 159 | status_code, result = _get_results(self._config, "servers/%s/actions/disable_rescue" % self.id, method="POST") 160 | if status_code != 201: 161 | raise HetznerActionException(result) 162 | 163 | self.rescue_enabled = False 164 | 165 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 166 | 167 | def enable_backups(self, backup_window=BACKUP_WINDOW_2AM_6AM): 168 | status_code, result = _get_results(self._config, "servers/%s/actions/enable_backup" % self.id, method="POST", 169 | body={"backup_window": backup_window}) 170 | if status_code != 201: 171 | raise HetznerActionException("Invalid backup window choice" if status_code == 422 else result) 172 | 173 | self.backup_window = backup_window 174 | 175 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 176 | 177 | def enable_rescue_mode(self, rescue_type=RESCUE_TYPE_LINUX, ssh_keys=[]): 178 | """ 179 | Enables rescue mode for the current server. 180 | 181 | NOTE: This will not reboot your server, you will need to do this either through the console or by calling the 182 | shutdown method. 183 | 184 | NOTE: Adding SSH keys to the rescue mode is only supported for RESCUE_TYPE_LINUX and RESCUE_TYPE_LINUX32. 185 | Attempting to pass an SSH key with another rescue type will result in it being ignored, and you having 186 | to log in with a root password. 187 | 188 | :param rescue_type: The rescue image to use. 189 | :param ssh_keys: An array of SSH key ids to load into the rescue mode (if it is linux based) 190 | :return: A tuple containing the root SSH password to access the recovery mode and the action to track the 191 | progress of the request. 192 | """ 193 | body = {"type": rescue_type} 194 | if ssh_keys and len(ssh_keys > 0) and rescue_type != RESCUE_TYPE_FREEBSD: 195 | body["ssh_keys"] = ssh_keys 196 | 197 | status_code, result = _get_results(self._config, "servers/%s/actions/enable_rescue" % self.id, method="POST", 198 | body=body) 199 | if status_code != 201: 200 | raise HetznerActionException(result) 201 | 202 | self.rescue_enabled = True 203 | 204 | return result["root_password"], HetznerCloudAction._load_from_json(self._config, result["action"]) 205 | 206 | def image(self, description=None, image_type="snapshot"): 207 | body = {"type": image_type} 208 | if description is not None: 209 | body["description"] = description 210 | 211 | status_code, result = _get_results(self._config, "servers/%s/actions/create_image" % self.id, method="POST", 212 | body=body) 213 | if status_code != 201: 214 | raise HetznerActionException(result) 215 | 216 | return result["image"]["id"], HetznerCloudAction._load_from_json(self._config, result["action"]) 217 | 218 | def power_on(self): 219 | status_code, result = _get_results(self._config, "servers/%s/actions/poweron" % self.id, method="POST") 220 | if status_code != 201: 221 | raise HetznerActionException(result) 222 | 223 | self.status = SERVER_STATUS_RUNNING 224 | 225 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 226 | 227 | def power_off(self): 228 | status_code, result = _get_results(self._config, "servers/%s/actions/poweroff" % self.id, method="POST") 229 | if status_code != 201: 230 | raise HetznerActionException(result) 231 | 232 | self.status = SERVER_STATUS_OFF 233 | 234 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 235 | 236 | def soft_reboot(self): 237 | status_code, result = _get_results(self._config, "servers/%s/actions/reboot" % self.id, method="POST") 238 | if status_code != 201: 239 | raise HetznerActionException(result) 240 | 241 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 242 | 243 | def rebuild_from_image(self, image): 244 | if not image: 245 | raise HetznerInvalidArgumentException("image") 246 | 247 | status_code, result = _get_results(self._config, "servers/%s/actions/rebuild" % self.id, method="POST", 248 | body={"image": image}) 249 | if status_code != 201: 250 | raise HetznerActionException(result) 251 | 252 | self.image_id = image 253 | 254 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 255 | 256 | def reset(self): 257 | status_code, result = _get_results(self._config, "servers/%s/actions/reset" % self.id, method="POST") 258 | if status_code != 201: 259 | raise HetznerActionException(result) 260 | 261 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 262 | 263 | def reset_root_password(self): 264 | status_code, result = _get_results(self._config, "servers/%s/actions/reset_password" % self.id, method="POST") 265 | if status_code != 201: 266 | raise HetznerActionException(result) 267 | 268 | return result["root_password"], HetznerCloudAction._load_from_json(self._config, result["action"]) 269 | 270 | def shutdown(self): 271 | status_code, result = _get_results(self._config, "servers/%s/actions/shutdown" % self.id, method="POST") 272 | if status_code != 201: 273 | raise HetznerActionException(result) 274 | 275 | return HetznerCloudAction._load_from_json(self._config, result["action"]) 276 | 277 | def wait_until_status_is(self, status, attempts=20, wait_seconds=1): 278 | """ 279 | Sleeps the executing thread (a second each loop) until the status is either what the user requires or the 280 | attempt count is exceeded, in which case an exception is thrown. 281 | 282 | :param status: The status the action needs to be. 283 | :param attempts: The number of attempts to query the action's status. 284 | :param wait_seconds: The number of seconds to wait for between each attempt. 285 | :return: An exception, unless the status matches the status parameter. 286 | """ 287 | if self.status == status: 288 | return 289 | 290 | for i in range(0, attempts): 291 | server_status = _get_server_json(self._config, self.id)["status"] 292 | if server_status == status: 293 | self.status = server_status 294 | return 295 | 296 | time.sleep(wait_seconds) 297 | 298 | raise HetznerWaitAttemptsExceededException() 299 | 300 | @staticmethod 301 | def _load_from_json(config, json, root_password=None): 302 | cloud_server = HetznerCloudServer(config) 303 | 304 | cloud_server.id = json["id"] 305 | cloud_server.name = json["name"] 306 | cloud_server.status = json["status"] 307 | cloud_server.created = json["created"] 308 | cloud_server.public_net_ipv4 = json["public_net"]["ipv4"]["ip"] 309 | cloud_server.public_net_ipv6 = json["public_net"]["ipv6"]["ip"] 310 | cloud_server.server_type = json["server_type"]["name"] 311 | cloud_server.datacenter_id = int(json["datacenter"]["id"] if json["datacenter"] is not None else 0) 312 | cloud_server.image_id = json["image"]["name"] if json["image"] is not None else "" 313 | cloud_server.iso_id = json["iso"]["name"] if json["iso"] is not None else "" 314 | cloud_server.rescue_enabled = bool(json["rescue_enabled"]) 315 | cloud_server.locked = bool(json["locked"]) 316 | cloud_server.backup_window = json["backup_window"] 317 | cloud_server.outgoing_traffic = int(json["outgoing_traffic"]) 318 | cloud_server.ingoing_traffic = int(json["ingoing_traffic"]) 319 | cloud_server.included_traffic = int(json["included_traffic"]) 320 | cloud_server.root_password = root_password 321 | 322 | return cloud_server 323 | -------------------------------------------------------------------------------- /hetznercloud/shared.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | from .exceptions import HetznerAuthenticationException, HetznerInternalServerErrorException, HetznerActionException, HetznerRateLimitExceeded 6 | 7 | 8 | def _get_results(config, endpoint, url_params=None, body=None, method="GET"): 9 | api = "https://api.hetzner.cloud/v%s/%s?" % (config.api_version, endpoint) 10 | headers = {"Authorization": "Bearer %s" % config.api_key} 11 | data = json.dumps(body) if body is not None else None 12 | 13 | if method == "GET": 14 | request = requests.get(api, headers=headers, params=url_params) 15 | elif method == "POST" or (method == "GET" and body is not None): 16 | request = requests.post(api, data=data, headers=headers, params=url_params) 17 | elif method == "DELETE": 18 | request = requests.delete(api, headers=headers) 19 | elif method == "PUT": 20 | request = requests.put(api, headers=headers, data=data) 21 | 22 | if request.status_code == 401 or request.status_code == 403: 23 | raise HetznerAuthenticationException() 24 | 25 | if request.status_code == 429: 26 | raise HetznerRateLimitExceeded() 27 | 28 | if request.status_code == 500: 29 | raise HetznerInternalServerErrorException(request.text) 30 | 31 | if not request.text: 32 | return request.status_code, "" 33 | 34 | try: 35 | js = request.json() 36 | if "action" in js and "error" in js["action"] and js["action"]["error"] is not None: 37 | raise HetznerActionException(js["action"]["error"]) 38 | 39 | return request.status_code, js 40 | except json.decoder.JSONDecodeError: 41 | raise HetznerInternalServerErrorException("failed to deserialise JSON") 42 | -------------------------------------------------------------------------------- /hetznercloud/ssh_keys.py: -------------------------------------------------------------------------------- 1 | from .exceptions import HetznerInvalidArgumentException, HetznerActionException 2 | from .shared import _get_results 3 | 4 | 5 | class HetznerCloudSSHKeysAction(object): 6 | def __init__(self, config): 7 | self._config = config 8 | 9 | def get_all(self, name=None): 10 | status_code, results = _get_results(self._config, "ssh_keys", 11 | url_params={"name": name} if name is not None else None) 12 | if status_code != 200: 13 | raise HetznerActionException(results) 14 | 15 | for result in results["ssh_keys"]: 16 | yield HetznerCloudSSHKey._load_from_json(self._config, result) 17 | 18 | def get(self, id): 19 | status_code, result = _get_results(self._config, "ssh_keys/%s" % id) 20 | if status_code != 200: 21 | raise HetznerActionException(result) 22 | 23 | return HetznerCloudSSHKey._load_from_json(self._config, result["ssh_key"]) 24 | 25 | def create(self, name, public_key): 26 | if not name: 27 | raise HetznerInvalidArgumentException("name") 28 | if not public_key: 29 | raise HetznerInvalidArgumentException("public_key") 30 | 31 | status_code, result = _get_results(self._config, "ssh_keys", method="POST", 32 | body={"name": name, "public_key": public_key}) 33 | if status_code != 201: 34 | raise HetznerActionException(result) 35 | 36 | return HetznerCloudSSHKey._load_from_json(self._config, result["ssh_key"]) 37 | 38 | 39 | class HetznerCloudSSHKey(object): 40 | def __init__(self, config): 41 | self._config = config 42 | self.id = 0 43 | self.name = "" 44 | self.fingerprint = "" 45 | self.public_key = "" 46 | 47 | def delete(self): 48 | status_code, result = _get_results(self._config, "ssh_keys/%s" % self.id, method="DELETE") 49 | if status_code != 204: 50 | raise HetznerActionException(result) 51 | 52 | def update(self, name): 53 | if not name: 54 | raise HetznerInvalidArgumentException("name") 55 | 56 | status_code, result = _get_results(self._config, "ssh_keys/%s" % self.id, method="PUT", 57 | body={"name": name}) 58 | if status_code != 200: 59 | raise HetznerActionException(result) 60 | 61 | self.name = name 62 | 63 | @staticmethod 64 | def _load_from_json(config, json): 65 | ssh_key = HetznerCloudSSHKey(config) 66 | 67 | ssh_key.id = int(json["id"]) 68 | ssh_key.name = json["name"] 69 | ssh_key.fingerprint = json["fingerprint"] 70 | ssh_key.public_key = json["public_key"] 71 | 72 | return ssh_key 73 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2018.1.18 2 | chardet==3.0.4 3 | idna==2.6 4 | nose==1.3.7 5 | requests==2.18.4 6 | urllib3==1.22 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="hetznercloud", 5 | packages=["hetznercloud"], 6 | version="1.1.1", 7 | description="Hetzner Cloud SDK", 8 | author="Liam Symonds", 9 | author_email="liam@ls-software.uk", 10 | url="https://github.com/elsyms/hetznercloud-py", 11 | keywords=["hetzner", "hetznercloud", "hetzner cloud api", "hetzner sdk", "hetzner api"], 12 | classifiers=[], 13 | install_requires=["requests==2.18.4"] 14 | ) 15 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from hetznercloud import HetznerCloudClient, SERVER_TYPE_1CPU_2GB, IMAGE_UBUNTU_1604, FLOATING_IP_TYPE_IPv4 4 | from .shared import valid_configuration 5 | 6 | 7 | class BaseHetznerTest(unittest.TestCase): 8 | def setUp(self): 9 | self.client = HetznerCloudClient(valid_configuration) 10 | self.servers = self.client.servers() 11 | 12 | def tearDown(self): 13 | for server in self.servers.get_all(): 14 | server.delete() 15 | for ssh_key in self.client.ssh_keys().get_all(): 16 | ssh_key.delete() 17 | for ip in self.client.floating_ips().get_all(): 18 | ip.delete() 19 | 20 | def create_server(self, name, start_after_create=True): 21 | return self.servers.create(name, SERVER_TYPE_1CPU_2GB, IMAGE_UBUNTU_1604, start_after_create=start_after_create) 22 | 23 | def create_floating_ip(self, description): 24 | return self.client.floating_ips().create(FLOATING_IP_TYPE_IPv4, home_location=1, description=description) -------------------------------------------------------------------------------- /tests/shared.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from hetznercloud import HetznerCloudClientConfiguration 4 | 5 | valid_configuration = HetznerCloudClientConfiguration().with_api_key(environ.get("HNER_API_KEY")).with_api_version(1) 6 | -------------------------------------------------------------------------------- /tests/test_configurations.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from hetznercloud import HetznerCloudClientConfiguration, HetznerCloudClient, HetznerConfigurationException 4 | 5 | 6 | class TestConfigurations(unittest.TestCase): 7 | def test_invalid_instance_of_configuration_results_in_an_exception(self): 8 | try: 9 | HetznerCloudClient(1231232) 10 | self.fail() 11 | except HetznerConfigurationException: 12 | pass 13 | 14 | def test_invalid_api_key_results_in_an_exception(self): 15 | try: 16 | HetznerCloudClient(HetznerCloudClientConfiguration().with_api_key(None)) 17 | self.fail() 18 | except HetznerConfigurationException: 19 | pass 20 | 21 | def test_invalid_api_version_results_in_an_exception(self): 22 | try: 23 | HetznerCloudClient(HetznerCloudClientConfiguration().with_api_key("abcdefg").with_api_version(2)) 24 | self.fail() 25 | except HetznerConfigurationException: 26 | pass -------------------------------------------------------------------------------- /tests/test_datacenters.py: -------------------------------------------------------------------------------- 1 | from tests.base import BaseHetznerTest 2 | 3 | 4 | class TestDatacenters(BaseHetznerTest): 5 | def test_can_get_all_datacenters(self): 6 | dcs = list(self.client.datacentres().get_all()) 7 | self.assertIsNotNone(dcs) 8 | self.assertTrue(len(dcs) > 0) 9 | 10 | def test_can_filter_datacenters_by_name(self): 11 | dcs = list(self.client.datacentres().get_all(name="fsn1-dc8")) 12 | self.assertIsNotNone(dcs) 13 | self.assertTrue(len(dcs) == 1) 14 | 15 | def test_can_get_dc_by_id(self): 16 | dc = self.client.datacentres().get(1) 17 | 18 | self.assertTrue(dc.id) 19 | self.assertTrue(dc.name) 20 | self.assertTrue(dc.description) 21 | self.assertIsNotNone(dc.location) 22 | self.assertTrue(len(dc.available_server_types) > 0) 23 | self.assertTrue(len(dc.supported_server_types) > 0) 24 | 25 | def test_can_get_all_datacenters_alias(self): 26 | """ 27 | Basic test for the datacenters alias method. 28 | """ 29 | dcs = list(self.client.datacenters().get_all()) 30 | self.assertIsNotNone(dcs) 31 | self.assertTrue(len(dcs) > 0) -------------------------------------------------------------------------------- /tests/test_floating_ips.py: -------------------------------------------------------------------------------- 1 | from hetznercloud import FLOATING_IP_TYPE_IPv4, SERVER_STATUS_RUNNING 2 | from tests.base import BaseHetznerTest 3 | 4 | 5 | class TestFloatingIps(BaseHetznerTest): 6 | def test_can_create_a_floating_ip(self): 7 | floating_ip = self.create_floating_ip("Test description") 8 | 9 | self.assertIsNotNone(floating_ip) 10 | self.assertEqual(floating_ip.description, "Test description") 11 | 12 | def test_can_assign_and_unassign_a_floating_ip_to_a_server(self): 13 | server, _ = self.create_server("test-assign-and-unassign-floating-ips") 14 | server.wait_until_status_is(SERVER_STATUS_RUNNING) 15 | 16 | floating_ip = self.create_floating_ip("Test description") 17 | 18 | floating_ip.assign_to_server(server.id) 19 | self.assertEqual(floating_ip.server, server.id) 20 | 21 | floating_ip.unassign_from_server() 22 | self.assertEqual(floating_ip.server, 0) 23 | 24 | def test_can_change_description_of_a_floating_ip(self): 25 | floating_ip = self.create_floating_ip("Test description") 26 | floating_ip.change_description("My new description") 27 | 28 | self.assertEqual(floating_ip.description, "My new description") 29 | 30 | def test_can_change_reverse_dns_record(self): 31 | floating_ip = self.create_floating_ip("Test description") 32 | floating_ip.change_reverse_dns_entry(floating_ip.ptr_ips[0]) -------------------------------------------------------------------------------- /tests/test_images.py: -------------------------------------------------------------------------------- 1 | from tests.base import BaseHetznerTest 2 | 3 | 4 | class TestImages(BaseHetznerTest): 5 | def test_can_get_all_images(self): 6 | images = list(self.client.images().get_all()) 7 | self.assertIsNotNone(images) 8 | self.assertTrue(len(images) > 0) 9 | 10 | def test_can_get_image_by_id(self): 11 | image = self.client.images().get(1) 12 | self.assertIsNotNone(image) -------------------------------------------------------------------------------- /tests/test_isos.py: -------------------------------------------------------------------------------- 1 | from tests.base import BaseHetznerTest 2 | 3 | 4 | class TestIsos(BaseHetznerTest): 5 | def test_can_get_all_isos(self): 6 | isos = list(self.client.isos().get_all()) 7 | self.assertIsNotNone(isos) 8 | self.assertTrue(len(isos) > 0) 9 | 10 | def test_can_filter_isos_by_name(self): 11 | isos = list(self.client.isos().get_all(name="virtio-win-0.1.141.iso")) 12 | self.assertIsNotNone(isos) 13 | self.assertTrue(len(isos) == 1) 14 | 15 | def test_can_get_iso_by_id(self): 16 | iso = self.client.isos().get(26) 17 | 18 | self.assertTrue(iso.id) 19 | self.assertTrue(iso.name) 20 | self.assertTrue(iso.description) 21 | self.assertTrue(iso.type) -------------------------------------------------------------------------------- /tests/test_locations.py: -------------------------------------------------------------------------------- 1 | from tests.base import BaseHetznerTest 2 | 3 | 4 | class TestLocations(BaseHetznerTest): 5 | def test_can_get_all_locations(self): 6 | locations = list(self.client.locations().get_all()) 7 | self.assertIsNotNone(locations) 8 | self.assertTrue(len(locations) > 0) 9 | 10 | def test_can_filter_locations_by_name(self): 11 | locations = list(self.client.locations().get_all(name="fsn1")) 12 | self.assertIsNotNone(locations) 13 | self.assertTrue(len(locations) == 1) 14 | 15 | def test_can_get_location_by_id(self): 16 | location = self.client.locations().get(1) 17 | 18 | self.assertTrue(location.id) 19 | self.assertTrue(location.name) 20 | self.assertTrue(location.description) 21 | self.assertTrue(location.country) 22 | self.assertTrue(location.city) 23 | self.assertTrue(location.latitude) 24 | self.assertTrue(location.longitude) -------------------------------------------------------------------------------- /tests/test_server_types.py: -------------------------------------------------------------------------------- 1 | from tests.base import BaseHetznerTest 2 | 3 | 4 | class TestServerTypes(BaseHetznerTest): 5 | def test_can_get_all_server_types(self): 6 | sts = list(self.client.server_types().get_all()) 7 | self.assertIsNotNone(sts) 8 | self.assertTrue(len(sts) > 0) 9 | 10 | def test_can_filter_server_types_by_name(self): 11 | sts = list(self.client.server_types().get_all(name="cx11")) 12 | self.assertIsNotNone(sts) 13 | self.assertTrue(len(sts) == 1) 14 | 15 | def test_can_get_server_type_by_id(self): 16 | st = self.client.server_types().get(1) 17 | 18 | self.assertTrue(st.id) 19 | self.assertTrue(st.name) 20 | self.assertTrue(st.description) 21 | self.assertTrue(st.cores) 22 | self.assertTrue(st.memory) 23 | self.assertTrue(st.disk) 24 | self.assertTrue(st.storage_type) -------------------------------------------------------------------------------- /tests/test_servers.py: -------------------------------------------------------------------------------- 1 | from hetznercloud import SERVER_STATUS_OFF, ACTION_STATUS_SUCCESS, SERVER_STATUS_RUNNING, \ 2 | BACKUP_WINDOW_10PM_2AM, HetznerActionException, SERVER_TYPE_2CPU_4GB, SERVER_TYPE_1CPU_2GB, IMAGE_UBUNTU_1604 3 | from tests.base import BaseHetznerTest 4 | 5 | 6 | class TestServers(BaseHetznerTest): 7 | 8 | def test_all_servers_can_be_retrieved(self): 9 | self.create_server("test-server-can-be-retrieved") 10 | 11 | all_servers = list(self.servers.get_all()) 12 | self.assertIsNotNone(all_servers) 13 | self.assertIsNot(0, len(all_servers)) 14 | 15 | def test_servers_can_be_retrieved_by_name(self): 16 | self.create_server("test-servers-can-be-retrieved-by-name") 17 | 18 | filtered_servers = list(self.servers.get_all("test-servers-can-be-retrieved-by-name")) 19 | self.assertIsNotNone(filtered_servers) 20 | self.assertIs(1, len(filtered_servers)) 21 | 22 | def test_server_can_be_created(self): 23 | created_server, _ = self.create_server("test-server-can-be-created") 24 | 25 | self.assertIsNotNone(created_server) 26 | self.assertIsNotNone(created_server.id) 27 | self.assertEqual(created_server.name, "test-server-can-be-created") 28 | self.assertEqual(created_server.image_id, IMAGE_UBUNTU_1604) 29 | self.assertEqual(created_server.server_type, SERVER_TYPE_1CPU_2GB) 30 | 31 | def test_server_can_be_created_but_offline(self): 32 | created_server, _ = self.create_server("test-server-can-be-created-offline", False) 33 | 34 | self.assertIsNotNone(created_server) 35 | self.assertIsNotNone(created_server.id) 36 | created_server.wait_until_status_is(SERVER_STATUS_OFF) 37 | 38 | def test_rescue_mode_can_be_added_to_a_server(self): 39 | created_server, _ = self.create_server("test-rescue-mode-can-be-added") 40 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 41 | 42 | root_password, action = created_server.enable_rescue_mode() 43 | self.assertIsNotNone(root_password) 44 | self.assertTrue(created_server.rescue_enabled) 45 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 46 | 47 | def test_rescue_mode_can_be_disabled_on_a_server(self): 48 | created_server, _ = self.create_server("test-rescue-mode-can-be-removed") 49 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 50 | 51 | _, action = created_server.enable_rescue_mode() 52 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 53 | self.assertTrue(created_server.rescue_enabled) 54 | 55 | action = created_server.disable_rescue_mode() 56 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 57 | self.assertFalse(created_server.rescue_enabled) 58 | 59 | def test_can_rename_a_server(self): 60 | created_server, _ = self.create_server("test-server-rename") 61 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 62 | 63 | created_server.change_name("renamed-server") 64 | self.assertEqual(created_server.name, "renamed-server") 65 | 66 | renamed_server = self.servers.get(created_server.id) 67 | self.assertEqual(renamed_server.name, "renamed-server") 68 | 69 | def test_invalid_backup_window_results_in_an_exception_being_thrown(self): 70 | created_server, _ = self.create_server("test-backup-window-errors") 71 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 72 | 73 | try: 74 | created_server.enable_backups("02-03") 75 | self.fail() 76 | except HetznerActionException: 77 | pass 78 | 79 | def test_can_enable_backups_with_a_valid_backup_window(self): 80 | created_server, _ = self.create_server("test-rescue-mode-can-be-enabled") 81 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 82 | 83 | created_server.enable_backups(BACKUP_WINDOW_10PM_2AM) 84 | 85 | self.assertEquals(created_server.backup_window, BACKUP_WINDOW_10PM_2AM) 86 | 87 | def test_can_attach_iso_to_a_server(self): 88 | created_server, _ = self.create_server("test-can-attach-iso") 89 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 90 | 91 | attach_iso_action = created_server.attach_iso("virtio-win-0.1.141.iso") 92 | attach_iso_action.wait_until_status_is(ACTION_STATUS_SUCCESS) 93 | self.assertEqual(created_server.iso, "virtio-win-0.1.141.iso") 94 | 95 | def test_can_change_reverse_dns_of_a_server(self): 96 | created_server, _ = self.create_server("test-can-change-reverse-dns") 97 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 98 | 99 | created_server.change_reverse_dns_entry(created_server.public_net_ipv4, "google.com") 100 | 101 | def test_can_change_server_type(self): 102 | created_server, _ = self.create_server("test-can-change-server-type") 103 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 104 | 105 | action = created_server.power_off() 106 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 107 | 108 | action = created_server.change_type(SERVER_TYPE_2CPU_4GB, False) 109 | self.assertEqual(created_server.server_type, SERVER_TYPE_2CPU_4GB) 110 | 111 | def test_can_power_off_a_server(self): 112 | created_server, _ = self.create_server("test-can-power-off-server") 113 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 114 | 115 | action = created_server.power_off() 116 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 117 | 118 | self.assertEqual(created_server.status, SERVER_STATUS_OFF) 119 | 120 | def test_can_power_on_a_server(self): 121 | created_server, _ = self.create_server("test-can-power-on-server") 122 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 123 | 124 | action = created_server.power_off() 125 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 126 | 127 | action = created_server.power_on() 128 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 129 | 130 | self.assertEqual(created_server.status, SERVER_STATUS_RUNNING) 131 | 132 | def test_can_rebuild_a_server(self): 133 | created_server, _ = self.create_server("test-can-rebuild-a-server") 134 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 135 | 136 | action = created_server.rebuild_from_image(IMAGE_UBUNTU_1604) 137 | action.wait_until_status_is(ACTION_STATUS_SUCCESS) 138 | 139 | self.assertEqual(created_server.image_id, IMAGE_UBUNTU_1604) 140 | 141 | def test_can_reset_a_server(self): 142 | created_server, _ = self.create_server("test-can-reset-a-server") 143 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 144 | 145 | created_server.reset() 146 | 147 | def test_can_reset_a_servers_root_password(self): 148 | created_server, _ = self.create_server("test-can-reset-a-servers-root-password") 149 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 150 | 151 | pw, _ = created_server.reset_root_password() 152 | self.assertIsNotNone(pw) 153 | 154 | def test_bad_response_code_results_in_an_exception_being_thrown(self): 155 | created_server, _ = self.create_server("test-bad-response-code-results-in-an-exception-being-thrown") 156 | created_server.wait_until_status_is(SERVER_STATUS_RUNNING) 157 | 158 | try: 159 | self.create_server("test-bad-response-code-results-in-an-exception-being-thrown") 160 | self.fail() 161 | except HetznerActionException: 162 | pass 163 | 164 | 165 | --------------------------------------------------------------------------------