././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8545113 hcloud-1.16.0/0000755000175100001710000000000000000000000012415 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/CHANGELOG.rst0000644000175100001710000001050600000000000014440 0ustar00runnerdocker======= History ======= v1.16.0 (2021-08-17) --------------------- * Feature: Add support for Load Balancer DNS PTRs v1.15.0 (2021-08-16) --------------------- * Feature: Add support for Placement Groups v1.14.1 (2021-08-10) --------------------- * Bugfix: Fix crash on extra fields in public_net response * Improvement: Format code with black v1.14.0 (2021-08-03) --------------------- * Feature: Add support for Firewall rule descriptions v1.13.0 (2021-07-16) --------------------- * Feature: Add support for Firewall Protocols ESP and GRE * Feature: Add support for Image Type APP * Feature: Add support for creating Firewalls with Firewalls * Feature: Add support for Label Selectors in Firewalls * Improvement: Improve handling of underlying TCP connections. Now for every client instance a single TCP connection is used instead of one per call. * Note: Support for Python 2.7 and Python 3.5 was removed v1.12.0 (2021-04-06) --------------------- * Feature: Add support for managed Certificates v1.11.0 (2021-03-11) --------------------- * Feature: Add support for Firewalls * Feature: Add `primary_disk_size` to `Server` Domain v1.10.0 (2020-11-03) --------------------- * Feature: Add `include_deprecated` filter to `get_list` and `get_all` on `ImagesClient` * Feature: Add vSwitch support to `add_subnet` on `NetworksClient` * Feature: Add subnet type constants to `NetworkSubnet` domain (`NetworkSubnet.TYPE_CLOUD`, `NetworkSubnet.TYPE_VSWITCH`) v1.9.1 (2020-08-11) -------------------- * Bugfix: BoundLoadBalancer serialization failed when using IP targets v1.9.0 (2020-08-10) -------------------- * Feature: Add `included_traffic`, `outgoing_traffic` and `ingoing_traffic` properties to Load Balancer domain * Feature: Add `change_type`-method to `LoadBalancersClient` * Feature: Add support for `LoadBalancerTargetLabelSelector` * Feature: Add support for `LoadBalancerTargetLabelSelector` v1.8.2 (2020-07-20) -------------------- * Fix: Loosen up the requirements. v1.8.1 (2020-06-29) -------------------- * Fix Load Balancer Client. * Fix: Unify setting of request parameters within `get_list` methods. 1.8.0 (2020-06-22) -------------------- * Feature: Add Load Balancers **Attention: The Load Balancer support in v1.8.0 is kind of broken. Please use v1.8.1** * Feature: Add Certificates 1.7.1 (2020-06-15) -------------------- * Feature: Add requests 2.23 support 1.7.0 (2020-06-05) -------------------- * Feature: Add support for the optional 'networks' parameter on server creation. * Feature: Add python 3.9 support * Feature: Add subnet type `cloud` 1.6.3 (2020-01-09) -------------------- * Feature: Add 'created' property to SSH Key domain * Fix: Remove ISODatetime Descriptor because it leads to wrong dates 1.6.2 (2019-10-15) ------------------- * Fix: future dependency requirement was too strict 1.6.1 (2019-10-01) ------------------- * Fix: python-dateutil dependency requirement was too strict 1.6.0 (2019-09-17) ------------------- * Feature: Add missing `get_by_name` on `FloatingIPsClient` 1.5.0 (2019-09-16) ------------------- * Fix: ServersClient.create_image fails when specifying the `labels` * Feature: Add support for `name` on Floating IPs 1.4.1 (2019-08-19) ------------------ * Fix: Documentation for `NetworkRoute` domain was missing * Fix: `requests` dependency requirement was to strict 1.4.0 (2019-07-29) ------------------ * Feature: Add `mac_address` to Server PrivateNet domain * Feature: Add python 3.8 support 1.3.0 (2019-07-10) ------------------ * Feature: Add status filter for servers, images and volumes * Feature: Add 'created' property to Floating IP domain * Feature: Add 'Networks' support 1.2.1 (2019-03-13) ------------------ * Fix: BoundVolume.server server property now casted to the 'BoundServer'. 1.2.0 (2019-03-06) ------------------ * Feature: Add `get_by_fingerprint`-method for ssh keys * Fix: Create Floating IP with location raises an error because no action was given. 1.1.0 (2019-02-27) ------------------ * Feature: Add `STATUS`-constants for server and volume status 1.0.1 (2019-02-22) ------------------ Fix: Ignore unknown fields in API response instead of raising an error 1.0.0 (2019-02-21) ------------------ * First stable release. You can find the documentation under https://hcloud-python.readthedocs.io/en/latest/ 0.1.0 (2018-12-20) ------------------ * First release on GitHub. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/CONTRIBUTING.rst0000644000175100001710000000721700000000000015065 0ustar00runnerdocker============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ----------------------- Report Bugs ~~~~~~~~~~~~ Report bugs at https://github.com/hetznercloud/hcloud-python/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~~ Hetzner Cloud Python could always use more documentation, whether as part of the official Hetzner Cloud Python docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/hetznercloud/hcloud-python/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------- Ready to contribute? Here's how to set up `hcloud-python` for local development. 1. Fork the `hcloud-python` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/hcloud-python.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: $ mkvirtualenv hcloud-python $ cd hcloud-python/ $ python setup.py develop 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: $ flake8 hetznercloud tests $ python setup.py test or py.test $ tox To get flake8 and tox, just pip install them into your virtualenv. 6. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. Pull Request Guidelines ------------------------ Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 2.7, 3.5 and 3.6, and for PyPy. Check https://travis-ci.org/hetznercloud/hcloud-python/pull_requests and make sure that the tests pass for all supported Python versions. Tips ----- To run a subset of tests:: $ py.test tests.test_hetznercloud How to release --------------- A reminder for the maintainers on how to release a new version. Make sure all your changes are committed (including an entry in CHANGELOG.rst). Then run:: 1. Change the version under /hcloud/version.py 2. Push the change to the `master` branch and tag an new release through the `Github UI `_. Travis will then deploy to PyPI if tests pass. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/LICENSE0000644000175100001710000000206500000000000013425 0ustar00runnerdockerMIT License Copyright (c) 2019, Hetzner Cloud GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/MANIFEST.in0000644000175100001710000000036400000000000014156 0ustar00runnerdockerinclude CONTRIBUTING.rst include CHANGELOG.rst include LICENSE include README.rst recursive-include tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8545113 hcloud-1.16.0/PKG-INFO0000644000175100001710000002175400000000000013523 0ustar00runnerdockerMetadata-Version: 2.1 Name: hcloud Version: 1.16.0 Summary: Official Hetzner Cloud python library Home-page: https://github.com/hetznercloud/hcloud-python Author: Hetzner Cloud GmbH Author-email: support-cloud@hetzner.com License: MIT license Description: Hetzner Cloud Python ==================== .. image:: https://github.com/hetznercloud/hcloud-python/workflows/Unit%20Tests/badge.svg :target: https://github.com/hetznercloud/hcloud-cloud-controller-manager/actions .. image:: https://github.com/hetznercloud/hcloud-python/workflows/Code%20Style/badge.svg :target: https://github.com/hetznercloud/hcloud-cloud-controller-manager/actions .. image:: https://readthedocs.org/projects/hcloud-python/badge/?version=latest :target: https://hcloud-python.readthedocs.io .. image:: https://img.shields.io/pypi/pyversions/hcloud.svg :target: https://pypi.org/project/hcloud/ Official Hetzner Cloud python library The library's documentation is available at `ReadTheDocs`_, the public API documentation is available at https://docs.hetzner.cloud. .. _ReadTheDocs: https://hcloud-python.readthedocs.io Usage example ------------- After the documentation has been created, click on `Usage` section Or open `docs/usage.rst` You can find some more examples under `examples/`. Supported Python versions ------------------------- We support python versions until `end-of-life`_. .. _end-of-life: https://devguide.python.org/#status-of-python-branches Development ----------- Setup Dev Environment --------------------- 1) `mkvirtualenv hcloud-python` 2) `pip install -e .` or `pip install -e .[docs]` to be able to build docs Run tests --------- * `tox .` * You can specify environment e.g `tox -e py36` * You can test the code style with `tox -e flake8` Create Documentation -------------------- Run `make docs`. This will also open a documentation in a tab in your default browser. Style Guide ------------- * **Type Hints**: If the type hint line is too long use inline hinting. Maximum inline type hint line should be 150 chars. License ------------- The MIT License (MIT). Please see `License File`_ for more information. .. _License File: https://github.com/hetznercloud/hcloud-python/blob/master/LICENSE ======= History ======= v1.16.0 (2021-08-17) --------------------- * Feature: Add support for Load Balancer DNS PTRs v1.15.0 (2021-08-16) --------------------- * Feature: Add support for Placement Groups v1.14.1 (2021-08-10) --------------------- * Bugfix: Fix crash on extra fields in public_net response * Improvement: Format code with black v1.14.0 (2021-08-03) --------------------- * Feature: Add support for Firewall rule descriptions v1.13.0 (2021-07-16) --------------------- * Feature: Add support for Firewall Protocols ESP and GRE * Feature: Add support for Image Type APP * Feature: Add support for creating Firewalls with Firewalls * Feature: Add support for Label Selectors in Firewalls * Improvement: Improve handling of underlying TCP connections. Now for every client instance a single TCP connection is used instead of one per call. * Note: Support for Python 2.7 and Python 3.5 was removed v1.12.0 (2021-04-06) --------------------- * Feature: Add support for managed Certificates v1.11.0 (2021-03-11) --------------------- * Feature: Add support for Firewalls * Feature: Add `primary_disk_size` to `Server` Domain v1.10.0 (2020-11-03) --------------------- * Feature: Add `include_deprecated` filter to `get_list` and `get_all` on `ImagesClient` * Feature: Add vSwitch support to `add_subnet` on `NetworksClient` * Feature: Add subnet type constants to `NetworkSubnet` domain (`NetworkSubnet.TYPE_CLOUD`, `NetworkSubnet.TYPE_VSWITCH`) v1.9.1 (2020-08-11) -------------------- * Bugfix: BoundLoadBalancer serialization failed when using IP targets v1.9.0 (2020-08-10) -------------------- * Feature: Add `included_traffic`, `outgoing_traffic` and `ingoing_traffic` properties to Load Balancer domain * Feature: Add `change_type`-method to `LoadBalancersClient` * Feature: Add support for `LoadBalancerTargetLabelSelector` * Feature: Add support for `LoadBalancerTargetLabelSelector` v1.8.2 (2020-07-20) -------------------- * Fix: Loosen up the requirements. v1.8.1 (2020-06-29) -------------------- * Fix Load Balancer Client. * Fix: Unify setting of request parameters within `get_list` methods. 1.8.0 (2020-06-22) -------------------- * Feature: Add Load Balancers **Attention: The Load Balancer support in v1.8.0 is kind of broken. Please use v1.8.1** * Feature: Add Certificates 1.7.1 (2020-06-15) -------------------- * Feature: Add requests 2.23 support 1.7.0 (2020-06-05) -------------------- * Feature: Add support for the optional 'networks' parameter on server creation. * Feature: Add python 3.9 support * Feature: Add subnet type `cloud` 1.6.3 (2020-01-09) -------------------- * Feature: Add 'created' property to SSH Key domain * Fix: Remove ISODatetime Descriptor because it leads to wrong dates 1.6.2 (2019-10-15) ------------------- * Fix: future dependency requirement was too strict 1.6.1 (2019-10-01) ------------------- * Fix: python-dateutil dependency requirement was too strict 1.6.0 (2019-09-17) ------------------- * Feature: Add missing `get_by_name` on `FloatingIPsClient` 1.5.0 (2019-09-16) ------------------- * Fix: ServersClient.create_image fails when specifying the `labels` * Feature: Add support for `name` on Floating IPs 1.4.1 (2019-08-19) ------------------ * Fix: Documentation for `NetworkRoute` domain was missing * Fix: `requests` dependency requirement was to strict 1.4.0 (2019-07-29) ------------------ * Feature: Add `mac_address` to Server PrivateNet domain * Feature: Add python 3.8 support 1.3.0 (2019-07-10) ------------------ * Feature: Add status filter for servers, images and volumes * Feature: Add 'created' property to Floating IP domain * Feature: Add 'Networks' support 1.2.1 (2019-03-13) ------------------ * Fix: BoundVolume.server server property now casted to the 'BoundServer'. 1.2.0 (2019-03-06) ------------------ * Feature: Add `get_by_fingerprint`-method for ssh keys * Fix: Create Floating IP with location raises an error because no action was given. 1.1.0 (2019-02-27) ------------------ * Feature: Add `STATUS`-constants for server and volume status 1.0.1 (2019-02-22) ------------------ Fix: Ignore unknown fields in API response instead of raising an error 1.0.0 (2019-02-21) ------------------ * First stable release. You can find the documentation under https://hcloud-python.readthedocs.io/en/latest/ 0.1.0 (2018-12-20) ------------------ * First release on GitHub. Keywords: hcloud hetzner cloud Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Requires-Python: !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11 Provides-Extra: docs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/README.rst0000644000175100001710000000367700000000000014121 0ustar00runnerdockerHetzner Cloud Python ==================== .. image:: https://github.com/hetznercloud/hcloud-python/workflows/Unit%20Tests/badge.svg :target: https://github.com/hetznercloud/hcloud-cloud-controller-manager/actions .. image:: https://github.com/hetznercloud/hcloud-python/workflows/Code%20Style/badge.svg :target: https://github.com/hetznercloud/hcloud-cloud-controller-manager/actions .. image:: https://readthedocs.org/projects/hcloud-python/badge/?version=latest :target: https://hcloud-python.readthedocs.io .. image:: https://img.shields.io/pypi/pyversions/hcloud.svg :target: https://pypi.org/project/hcloud/ Official Hetzner Cloud python library The library's documentation is available at `ReadTheDocs`_, the public API documentation is available at https://docs.hetzner.cloud. .. _ReadTheDocs: https://hcloud-python.readthedocs.io Usage example ------------- After the documentation has been created, click on `Usage` section Or open `docs/usage.rst` You can find some more examples under `examples/`. Supported Python versions ------------------------- We support python versions until `end-of-life`_. .. _end-of-life: https://devguide.python.org/#status-of-python-branches Development ----------- Setup Dev Environment --------------------- 1) `mkvirtualenv hcloud-python` 2) `pip install -e .` or `pip install -e .[docs]` to be able to build docs Run tests --------- * `tox .` * You can specify environment e.g `tox -e py36` * You can test the code style with `tox -e flake8` Create Documentation -------------------- Run `make docs`. This will also open a documentation in a tab in your default browser. Style Guide ------------- * **Type Hints**: If the type hint line is too long use inline hinting. Maximum inline type hint line should be 150 chars. License ------------- The MIT License (MIT). Please see `License File`_ for more information. .. _License File: https://github.com/hetznercloud/hcloud-python/blob/master/LICENSE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8425112 hcloud-1.16.0/docs/0000755000175100001710000000000000000000000013345 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/Makefile0000644000175100001710000000113700000000000015007 0ustar00runnerdocker# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = hcloud SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.actions.rst0000644000175100001710000000034000000000000017744 0ustar00runnerdockerActionsClient ================== .. autoclass:: hcloud.actions.client.ActionsClient :members: .. autoclass:: hcloud.actions.client.BoundAction :members: .. autoclass:: hcloud.actions.domain.Action :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.certificates.rst0000644000175100001710000000040200000000000020750 0ustar00runnerdockerCertificateClient ================== .. autoclass:: hcloud.certificates.client.CertificatesClient :members: .. autoclass:: hcloud.certificates.client.BoundCertificate :members: .. autoclass:: hcloud.certificates.domain.Certificate :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.datacenters.rst0000644000175100001710000000051200000000000020602 0ustar00runnerdockerDatacentersClient ================== .. autoclass:: hcloud.datacenters.client.DatacentersClient :members: .. autoclass:: hcloud.datacenters.client.BoundDatacenter :members: .. autoclass:: hcloud.datacenters.domain.Datacenter :members: .. autoclass:: hcloud.datacenters.domain.DatacenterServerTypes :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.firewalls.rst0000644000175100001710000000070500000000000020301 0ustar00runnerdockerFirewallsClient ================== .. autoclass:: hcloud.firewalls.client.FirewallsClient :members: .. autoclass:: hcloud.firewalls.client.BoundFirewall :members: .. autoclass:: hcloud.firewalls.domain.Firewall :members: .. autoclass:: hcloud.firewalls.domain.FirewallRule :members: .. autoclass:: hcloud.firewalls.domain.FirewallResource :members: .. autoclass:: hcloud.firewalls.domain.CreateFirewallResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.floating_ips.rst0000644000175100001710000000052100000000000020763 0ustar00runnerdockerFloating IPsClient ================== .. autoclass:: hcloud.floating_ips.client.FloatingIPsClient :members: .. autoclass:: hcloud.floating_ips.client.BoundFloatingIP :members: .. autoclass:: hcloud.floating_ips.domain.FloatingIP :members: .. autoclass:: hcloud.floating_ips.domain.CreateFloatingIPResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.images.rst0000644000175100001710000000044000000000000017552 0ustar00runnerdockerImagesClient ================== .. autoclass:: hcloud.images.client.ImagesClient :members: .. autoclass:: hcloud.images.client.BoundImage :members: .. autoclass:: hcloud.images.domain.Image :members: .. autoclass:: hcloud.images.domain.CreateImageResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.isos.rst0000644000175100001710000000031300000000000017261 0ustar00runnerdockerISOsClient ================== .. autoclass:: hcloud.isos.client.IsosClient :members: .. autoclass:: hcloud.isos.client.BoundIso :members: .. autoclass:: hcloud.isos.domain.Iso :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.load_balancer_types.rst0000644000175100001710000000033300000000000022300 0ustar00runnerdockerLoadBalancerTypesClient ======================== .. autoclass:: hcloud.load_balancer_types.client.LoadBalancerTypesClient :members: .. autoclass:: hcloud.load_balancer_types.domain.LoadBalancerType :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.load_balancers.rst0000644000175100001710000000164000000000000021241 0ustar00runnerdockerLoadBalancerClient ================== .. autoclass:: hcloud.load_balancers.client.LoadBalancersClient :members: .. autoclass:: hcloud.load_balancers.client.BoundLoadBalancer :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancer :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerService :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerServiceHttp :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerHealthCheck :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerHealtCheckHttp :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerTarget :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerTargetLabelSelector :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerTargetIP :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerAlgorithm :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.locations.rst0000644000175100001710000000035600000000000020306 0ustar00runnerdockerLocationsClient ================== .. autoclass:: hcloud.locations.client.LocationsClient :members: .. autoclass:: hcloud.locations.client.BoundLocation :members: .. autoclass:: hcloud.locations.domain.Location :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.networks.rst0000644000175100001710000000066600000000000020173 0ustar00runnerdockerNetworksClient ================== .. autoclass:: hcloud.networks.client.NetworksClient :members: .. autoclass:: hcloud.networks.client.BoundNetwork :members: .. autoclass:: hcloud.networks.domain.Network :members: .. autoclass:: hcloud.networks.domain.NetworkSubnet :members: .. autoclass:: hcloud.networks.domain.NetworkRoute :members: .. autoclass:: hcloud.networks.domain.CreateNetworkResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.placement_groups.rst0000644000175100001710000000056400000000000021663 0ustar00runnerdockerPlacementGroupsClient ================== .. autoclass:: hcloud.placement_groups.client.PlacementGroupsClient :members: .. autoclass:: hcloud.placement_groups.client.BoundPlacementGroup :members: .. autoclass:: hcloud.placement_groups.domain.PlacementGroup :members: .. autoclass:: hcloud.placement_groups.domain.CreatePlacementGroupResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.server_types.rst0000644000175100001710000000037600000000000021047 0ustar00runnerdockerServerTypesClient ================== .. autoclass:: hcloud.server_types.client.ServerTypesClient :members: .. autoclass:: hcloud.server_types.client.BoundServerType :members: .. autoclass:: hcloud.server_types.domain.ServerType :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.servers.rst0000644000175100001710000000131000000000000017773 0ustar00runnerdockerServersClient ================== .. autoclass:: hcloud.servers.client.ServersClient :members: .. autoclass:: hcloud.servers.client.BoundServer :members: .. autoclass:: hcloud.servers.domain.Server :members: .. autoclass:: hcloud.servers.domain.PublicNetwork :members: .. autoclass:: hcloud.servers.domain.IPv4Address :members: .. autoclass:: hcloud.servers.domain.IPv6Network :members: .. autoclass:: hcloud.servers.domain.CreateServerResponse :members: .. autoclass:: hcloud.servers.domain.ResetPasswordResponse :members: .. autoclass:: hcloud.servers.domain.EnableRescueResponse :members: .. autoclass:: hcloud.servers.domain.RequestConsoleResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.ssh_keys.rst0000644000175100001710000000034300000000000020137 0ustar00runnerdockerSSHKeysClient ================== .. autoclass:: hcloud.ssh_keys.client.SSHKeysClient :members: .. autoclass:: hcloud.ssh_keys.client.BoundSSHKey :members: .. autoclass:: hcloud.ssh_keys.domain.SSHKey :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.clients.volumes.rst0000644000175100001710000000045000000000000020000 0ustar00runnerdockerVolumesClient ================== .. autoclass:: hcloud.volumes.client.VolumesClient :members: .. autoclass:: hcloud.volumes.client.BoundVolume :members: .. autoclass:: hcloud.volumes.domain.Volume :members: .. autoclass:: hcloud.volumes.domain.CreateVolumeResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/api.rst0000644000175100001710000000065100000000000014652 0ustar00runnerdockerhcloud-python API ================== Main Interface --------------- .. autoclass:: hcloud.Client :members: API Clients ------------- .. toctree:: :maxdepth: 3 :glob: api.clients.* Exceptions --------------- .. autoclass:: hcloud.APIException :members: .. autoclass:: hcloud.actions.domain.ActionFailedException :members: .. autoclass:: hcloud.actions.domain.ActionTimeoutException :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/changelog.rst0000644000175100001710000000003600000000000016025 0ustar00runnerdocker.. include:: ../CHANGELOG.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/conf.py0000755000175100001710000001203200000000000014645 0ustar00runnerdocker#!/usr/bin/env python # -*- coding: utf-8 -*- # # hcloud documentation build configuration file, created by # sphinx-quickstart on Fri Jun 9 13:47:02 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another # directory, add these directories to sys.path here. If the directory is # relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath("..")) from hcloud.__version__ import VERSION # noqa # -- General configuration --------------------------------------------- # If your documentation needs a minimal Sphinx _version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] # source_suffix = '.rst' # The master toctree document. master_doc = "index" # General information about the project. project = u"Hetzner Cloud Python" copyright = u"2019, Hetzner Cloud GmbH" author = u"Hetzner Cloud GmbH" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout # the built documents. # # The short X.Y _version. version = VERSION # The full _version, including alpha/beta/rc tags. release = VERSION # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # -- Options for HTML output ------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" html_logo = "_static/logo-hetzner-online.svg" # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the # documentation. # html_theme_options = { "logo_only": True, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # -- Options for HTMLHelp output --------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = "hclouddoc" # -- Options for LaTeX output ------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto, manual, or own class]). latex_documents = [ ( master_doc, "hcloud.tex", u"Hetzner Cloud Python Documentation", u"Hetzner Cloud GmbH", "manual", ), ] # -- Options for manual page output ------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, u"Hetzner Cloud Python Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, u"Hetzner Cloud Python Documentation", author, "HCloud-python is a library for the Hetzner Cloud API.", "Miscellaneous", ), ] source_suffix = [".rst"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/contributing.rst0000644000175100001710000000004100000000000016601 0ustar00runnerdocker.. include:: ../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/index.rst0000644000175100001710000000327600000000000015216 0ustar00runnerdocker.. toctree:: :maxdepth: 4 :hidden: self installation samples api Hetzner Cloud API Documentation contributing changelog Hetzner Cloud Python ==================== .. image:: https://travis-ci.com/hetznercloud/hcloud-python.svg?branch=master :target: https://travis-ci.com/hetznercloud/hcloud-python .. image:: https://readthedocs.org/projects/hcloud-python/badge/?version=latest :target: https://hcloud-python.readthedocs.io This is the official `Hetzner Cloud`_ python library. .. _Hetzner Cloud: https://www.hetzner.com/cloud Examples ------------- Create Server ------------- .. code-block:: python :linenos: from hcloud import Client from hcloud.server_types.domain import ServerType from hcloud.images.domain import Image client = Client(token="{YOUR_API_TOKEN}") # Please paste your API token here between the quotes response = client.servers.create(name="my-server", server_type=ServerType(name="cx11"), image=Image(name="ubuntu-20.04")) server = response.server print(server) print("Root Password: " + response.root_password) List Servers ------------ .. code-block:: python :linenos: from hcloud import Client client = Client(token="{YOUR_API_TOKEN}") # Please paste your API token here between the quotes servers = client.servers.get_all() print(servers) You can find more examples in the `Example Folder`_ in the Github Repository. .. _Example Folder: https://github.com/hetznercloud/hcloud-python/tree/master/examples License ------- The MIT License (MIT). Please see `License File`_ for more information. .. _License File: https://github.com/hetznercloud/hcloud-python/blob/master/LICENSE ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/installation.rst0000644000175100001710000000225000000000000016577 0ustar00runnerdocker.. highlight:: shell ============ Installation ============ Stable release -------------- To install Hetzner Cloud Python, run this command in your terminal: .. code-block:: console $ pip install hcloud This is the preferred method to install Hetzner Cloud Python, as it will always install the most recent stable release. If you don't have `pip`_ installed, this `Python installation guide`_ can guide you through the process. .. _pip: https://pip.pypa.io .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ From sources ------------ The sources for Hetzner Cloud Python can be downloaded from the `Github repo`_. You can either clone the public repository: .. code-block:: console $ git clone git://github.com/hetznercloud/hcloud-python Or download the `tarball`_: .. code-block:: console $ curl -OL https://github.com/hetznercloud/hcloud-python/tarball/master Once you have a copy of the source, you can install it with: .. code-block:: console $ python setup.py install .. _Github repo: https://github.com/hetznercloud/hcloud-python .. _tarball: https://github.com/hetznercloud/hcloud-python/tarball/master ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/make.bat0000644000175100001710000000140000000000000014745 0ustar00runnerdocker@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=python -msphinx ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=hcloud if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The Sphinx module was not found. Make sure you have Sphinx installed, echo.then set the SPHINXBUILD environment variable to point to the full echo.path of the 'sphinx-build' executable. Alternatively you may add the echo.Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/docs/samples.rst0000644000175100001710000000271600000000000015551 0ustar00runnerdocker======== Samples ======== To use Hetzner Cloud Python in a project: .. code-block:: python from hcloud import Client from hcloud.images.domain import Image from hcloud.server_types.domain import ServerType # Create a client client = Client(token="project-token") # Create 2 servers # Create 2 servers response1 = client.servers.create( "Server1", server_type=ServerType(name="cx11"), image=Image(id=4711) ) response2 = client.servers.create( "Server2", server_type=ServerType(name="cx11"), image=Image(id=4711) ) # Get all servers server1 = response1.server server2 = response2.server servers = client.servers.get_all() assert servers[0].id == server1.id assert servers[1].id == server2.id # Create 2 volumes response1 = client.volumes.create( size=15, name="Volume1", location=server1.location ) response2 = client.volumes.create( size=10, name="Volume2", location=server2.location ) volume1 = response1.volume volume2 = response2.volume # Attach volume to server volume1.attach(server1) volume2.attach(server2) # Detach second volume volume2.detach() # Poweroff 2nd server server2.power_off() # Poweroff 2nd server server2.power_off() More samples are in the repository: https://github.com/hetznercloud/hcloud-python/tree/master/examples.././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8425112 hcloud-1.16.0/hcloud/0000755000175100001710000000000000000000000013673 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/__init__.py0000644000175100001710000000011200000000000015776 0ustar00runnerdocker# -*- coding: utf-8 -*- from .hcloud import Client, APIException # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/__version__.py0000644000175100001710000000002300000000000016521 0ustar00runnerdockerVERSION = "1.16.0" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8425112 hcloud-1.16.0/hcloud/actions/0000755000175100001710000000000000000000000015333 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/actions/__init__.py0000644000175100001710000000003000000000000017435 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/actions/client.py0000644000175100001710000000760300000000000017171 0ustar00runnerdocker# -*- coding: utf-8 -*- import time from hcloud.core.client import ClientEntityBase, BoundModelBase from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException class BoundAction(BoundModelBase): model = Action def wait_until_finished(self, max_retries=100): """Wait until the specific action has status="finished" (set Client.poll_interval to specify a delay between checks) :param max_retries: int Specify how many retries will be performed before an ActionTimeoutException will be raised :raises: ActionFailedException when action is finished with status=="error" :raises: ActionTimeoutException when Action is still in "running" state after max_retries reloads. """ while self.status == Action.STATUS_RUNNING: if max_retries > 0: self.reload() time.sleep(self._client._client.poll_interval) max_retries = max_retries - 1 else: raise ActionTimeoutException(action=self) if self.status == Action.STATUS_ERROR: raise ActionFailedException(action=self) class ActionsClient(ClientEntityBase): results_list_attribute_name = "actions" def get_by_id(self, id): # type: (int) -> BoundAction """Get a specific action by its ID. :param id: int :return: :class:`BoundAction ` """ response = self._client.request( url="/actions/{action_id}".format(action_id=id), method="GET" ) return BoundAction(self, response["action"]) def get_list( self, status=None, # type: Optional[List[str]] sort=None, # type: Optional[List[str]] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundAction]] """Get a list of actions from this account :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ params = {} if status is not None: params["status"] = status if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url="/actions", method="GET", params=params) actions = [ BoundAction(self, action_data) for action_data in response["actions"] ] return self._add_meta_to_result(actions, response) def get_all(self, status=None, sort=None): # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Get all actions of the account :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) :return: List[:class:`BoundAction `] """ return super(ActionsClient, self).get_all(status=status, sort=sort) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/actions/domain.py0000644000175100001710000000350400000000000017156 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain class Action(BaseDomain): """Action Domain :param id: int ID of an action :param command: Command executed in the action :param status: Status of the action :param progress: Progress of action in percent :param started: Point in time when the action was started :param datetime,None finished: Point in time when the action was finished. Only set if the action is finished otherwise None :param resources: Resources the action relates to :param error: Error message for the action if error occurred, otherwise None. """ STATUS_RUNNING = "running" """Action Status running""" STATUS_SUCCESS = "success" """Action Status success""" STATUS_ERROR = "error" """Action Status error""" __slots__ = ( "id", "command", "status", "progress", "resources", "error", "started", "finished", ) def __init__( self, id, command=None, status=None, progress=None, started=None, finished=None, resources=None, error=None, ): self.id = id self.command = command self.status = status self.progress = progress self.started = isoparse(started) if started else None self.finished = isoparse(finished) if finished else None self.resources = resources self.error = error class ActionFailedException(Exception): """The Action you was waiting for failed""" def __init__(self, action): self.action = action class ActionTimeoutException(Exception): """The Action you was waiting for timeouted in hcloud-python.""" def __init__(self, action): self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8425112 hcloud-1.16.0/hcloud/certificates/0000755000175100001710000000000000000000000016340 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/certificates/__init__.py0000644000175100001710000000003000000000000020442 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/certificates/client.py0000644000175100001710000003536500000000000020204 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.certificates.domain import ( Certificate, CreateManagedCertificateResponse, ManagedCertificateStatus, ManagedCertificateError, ) from hcloud.core.domain import add_meta_to_result class BoundCertificate(BoundModelBase): model = Certificate def __init__(self, client, data, complete=True): status = data.get("status") if status is not None: error_data = status.get("error") error = None if error_data: error = ManagedCertificateError( code=error_data["code"], message=error_data["message"] ) data["status"] = ManagedCertificateStatus( issuance=status["issuance"], renewal=status["renewal"], error=error ) super(BoundCertificate, self).__init__(client, data, complete) def get_actions_list(self, status=None, sort=None, page=None, per_page=None): # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] """Returns all action objects for a Certificate. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a Certificate. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) def update(self, name=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]]) -> BoundCertificate """Updates an certificate. You can update an certificate name and the certificate labels. :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundCertificate """ return self._client.update(self, name, labels) def delete(self): # type: () -> bool """Deletes a certificate. :return: boolean """ return self._client.delete(self) def retry_issuance(self): # type: () -> BoundAction """Retry a failed Certificate issuance or renewal. :return: BoundAction """ return self._client.retry_issuance(self) class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "certificates" def get_by_id(self, id): # type: (int) -> BoundCertificate """Get a specific certificate by its ID. :param id: int :return: :class:`BoundCertificate ` """ response = self._client.request( url="/certificates/{certificate_id}".format(certificate_id=id), method="GET" ) return BoundCertificate(self, response["certificate"]) def get_list( self, name=None, # type: Optional[str] label_selector=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundCertificate], Meta] """Get a list of certificates :param name: str (optional) Can be used to filter certificates by their name. :param label_selector: str (optional) Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundCertificate `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if label_selector is not None: params["label_selector"] = label_selector if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/certificates", method="GET", params=params ) certificates = [ BoundCertificate(self, certificate_data) for certificate_data in response["certificates"] ] return self._add_meta_to_result(certificates, response) def get_all(self, name=None, label_selector=None): # type: (Optional[str]) -> List[BoundCertificate] """Get all certificates :param name: str (optional) Can be used to filter certificates by their name. :param label_selector: str (optional) Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. :return: List[:class:`BoundCertificate `] """ return super(CertificatesClient, self).get_all( name=name, label_selector=label_selector ) def get_by_name(self, name): # type: (str) -> BoundCertificate """Get certificate by name :param name: str Used to get certificate by name. :return: :class:`BoundCertificate ` """ return super(CertificatesClient, self).get_by_name(name) def create(self, name, certificate, private_key, labels=None): # type: (str, str, Optional[Dict[str, str]]) -> BoundCertificate """Creates a new Certificate with the given name, certificate and private_key. This methods allows only creating custom uploaded certificates. If you want to create a managed certificate use :func:`~hcloud.certificates.client.CertificatesClient.create_managed` :param name: str :param certificate: str Certificate and chain in PEM format, in order so that each record directly certifies the one preceding :param private_key: str Certificate key in PEM format :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ data = { "name": name, "certificate": certificate, "private_key": private_key, "type": Certificate.TYPE_UPLOADED, } if labels is not None: data["labels"] = labels response = self._client.request(url="/certificates", method="POST", json=data) return BoundCertificate(self, response["certificate"]) def create_managed(self, name, domain_names, labels=None): # type: (str, List[str], Optional[Dict[str, str]]) -> CreateManagedCertificateResponse """Creates a new managed Certificate with the given name and domain names. This methods allows only creating managed certificates for domains that are using the Hetzner DNS service. If you want to create a custom uploaded certificate use :func:`~hcloud.certificates.client.CertificatesClient.create` :param name: str :param domain_names: List[str] Domains and subdomains that should be contained in the Certificate :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ data = { "name": name, "type": Certificate.TYPE_MANAGED, "domain_names": domain_names, } if labels is not None: data["labels"] = labels response = self._client.request(url="/certificates", method="POST", json=data) return CreateManagedCertificateResponse( certificate=BoundCertificate(self, response["certificate"]), action=BoundAction(self._client.actions, response["action"]), ) def update(self, certificate, name=None, labels=None): # type: (Certificate, Optional[str], Optional[Dict[str, str]]) -> BoundCertificate """Updates a Certificate. You can update a certificate name and labels. :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ data = {} if name is not None: data["name"] = name if labels is not None: data["labels"] = labels response = self._client.request( url="/certificates/{certificate_id}".format(certificate_id=certificate.id), method="PUT", json=data, ) return BoundCertificate(self, response["certificate"]) def delete(self, certificate): # type: (Certificate) -> bool self._client.request( url="/certificates/{certificate_id}".format(certificate_id=certificate.id), method="DELETE", ) """Deletes a certificate. :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` :return: True """ # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised return True def get_actions_list( self, certificate, status=None, sort=None, page=None, per_page=None ): # type: (Certificate, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] """Returns all action objects for a Certificate. :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ params = {} if status is not None: params["status"] = status if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/certificates/{certificate_id}/actions".format( certificate_id=certificate.id ), method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return add_meta_to_result(actions, response, "actions") def get_actions(self, certificate, status=None, sort=None): # type: (Certificate, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a Certificate. :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return super(CertificatesClient, self).get_actions( certificate, status=status, sort=sort ) def retry_issuance(self, certificate): # type: (Certificate) -> BoundAction """Returns all action objects for a Certificate. :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` :return: :class:`BoundAction ` """ response = self._client.request( url="/certificates/{certificate_id}/actions/retry".format( certificate_id=certificate.id ), method="POST", ) return BoundAction(self._client.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/certificates/domain.py0000644000175100001710000000720400000000000020164 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin class Certificate(BaseDomain, DomainIdentityMixin): """Certificate Domain :param id: int ID of Certificate :param name: str Name of Certificate :param certificate: str Certificate and chain in PEM format, in order so that each record directly certifies the one preceding :param not_valid_before: datetime Point in time when the Certificate becomes valid :param not_valid_after: datetime Point in time when the Certificate becomes invalid :param domain_names: List[str] List of domains and subdomains covered by this certificate :param fingerprint: str Fingerprint of the Certificate :param labels: dict User-defined labels (key-value pairs) :param created: datetime Point in time when the certificate was created :param type: str Type of Certificate :param status: ManagedCertificateStatus Current status of a type managed Certificate, always none for type uploaded Certificates """ __slots__ = ( "id", "name", "certificate", "not_valid_before", "not_valid_after", "domain_names", "fingerprint", "created", "labels", "type", "status", ) TYPE_UPLOADED = "uploaded" TYPE_MANAGED = "managed" def __init__( self, id=None, name=None, certificate=None, not_valid_before=None, not_valid_after=None, domain_names=None, fingerprint=None, created=None, labels=None, type=None, status=None, ): self.id = id self.name = name self.type = type self.certificate = certificate self.domain_names = domain_names self.fingerprint = fingerprint self.not_valid_before = isoparse(not_valid_before) if not_valid_before else None self.not_valid_after = isoparse(not_valid_after) if not_valid_after else None self.created = isoparse(created) if created else None self.labels = labels self.status = status class ManagedCertificateStatus(BaseDomain): """ManagedCertificateStatus Domain :param issuance: str Status of the issuance process of the Certificate :param renewal: str Status of the renewal process of the Certificate :param error: ManagedCertificateError If issuance or renewal reports failure, this property contains information about what happened """ def __init__(self, issuance=None, renewal=None, error=None): self.issuance = issuance self.renewal = renewal self.error = error class ManagedCertificateError(BaseDomain): """ManagedCertificateError Domain :param code: str Error code identifying the error :param message: Message detailing the error """ def __init__(self, code=None, message=None): self.code = code self.message = message class CreateManagedCertificateResponse(BaseDomain): """Create Managed Certificate Response Domain :param certificate: :class:`BoundCertificate ` The created server :param action: :class:`BoundAction ` Shows the progress of the certificate creation """ __slots__ = ( "certificate", "action", ) def __init__( self, certificate, # type: BoundCertificate action, # type: BoundAction ): self.certificate = certificate self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/core/0000755000175100001710000000000000000000000014623 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/core/__init__.py0000644000175100001710000000003000000000000016725 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/core/client.py0000644000175100001710000000742000000000000016456 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.domain import add_meta_to_result class ClientEntityBase(object): max_per_page = 50 results_list_attribute_name = None def __init__(self, client): """ :param client: Client :return self """ self._client = client def _is_list_attribute_implemented(self): if self.results_list_attribute_name is None: raise NotImplementedError( "in order to get results list, 'results_list_attribute_name' attribute of {} has to be specified".format( self.__class__.__name__ ) ) def _add_meta_to_result( self, results, # type: List[BoundModelBase] response, # type: json ): # type: (...) -> PageResult self._is_list_attribute_implemented() return add_meta_to_result(results, response, self.results_list_attribute_name) def _get_all( self, list_function, # type: function results_list_attribute_name, # type: str *args, **kwargs ): # type (...) -> List[BoundModelBase] page = 1 results = [] while page: page_result = list_function( page=page, per_page=self.max_per_page, *args, **kwargs ) result = getattr(page_result, results_list_attribute_name) if result: results.extend(result) meta = page_result.meta if ( meta and meta.pagination and meta.pagination.next_page and meta.pagination.next_page ): page = meta.pagination.next_page else: page = None return results def get_all(self, *args, **kwargs): # type: (...) -> List[BoundModelBase] self._is_list_attribute_implemented() return self._get_all( self.get_list, self.results_list_attribute_name, *args, **kwargs ) def get_actions(self, *args, **kwargs): # type: (...) -> List[BoundModelBase] if not hasattr(self, "get_actions_list"): raise ValueError("this endpoint does not support get_actions method") return self._get_all(self.get_actions_list, "actions", *args, **kwargs) class GetEntityByNameMixin(object): """ Use as a mixin for ClientEntityBase classes """ def get_by_name(self, name): # type: (str) -> BoundModelBase self._is_list_attribute_implemented() response = self.get_list(name=name) entities = getattr(response, self.results_list_attribute_name) entity = entities[0] if entities else None return entity class BoundModelBase(object): """Bound Model Base""" model = None def __init__(self, client, data={}, complete=True): """ :param client: The client for the specific model to use :param data: The data of the model :param complete: bool False if not all attributes of the model fetched """ self._client = client self.complete = complete self.data_model = self.model.from_dict(data) def __getattr__(self, name): """Allow magical access to the properties of the model :param name: str :return: """ value = getattr(self.data_model, name) if not value and not self.complete: self.reload() value = getattr(self.data_model, name) return value def reload(self): """Reloads the model and tries to get all data from the APIx""" bound_model = self._client.get_by_id(self.data_model.id) self.data_model = bound_model.data_model self.complete = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/core/domain.py0000644000175100001710000000373000000000000016447 0ustar00runnerdocker# -*- coding: utf-8 -*- from collections import namedtuple class BaseDomain(object): __slots__ = () @classmethod def from_dict(cls, data): supported_data = {k: v for k, v in data.items() if k in cls.__slots__} return cls(**supported_data) class DomainIdentityMixin(object): __slots__ = () @property def id_or_name(self): if self.id is not None: return self.id elif self.name is not None: return self.name else: raise ValueError("id or name must be set") class Pagination(BaseDomain): __slots__ = ( "page", "per_page", "previous_page", "next_page", "last_page", "total_entries", ) def __init__( self, page, per_page, previous_page=None, next_page=None, last_page=None, total_entries=None, ): self.page = page self.per_page = per_page self.previous_page = previous_page self.next_page = next_page self.last_page = last_page self.total_entries = total_entries class Meta(BaseDomain): __slots__ = ("pagination",) def __init__( self, pagination=None, ): self.pagination = pagination @classmethod def parse_meta(cls, json_content): meta = None if json_content and "meta" in json_content: meta = cls() pagination_json = json_content["meta"].get("pagination") if pagination_json: pagination = Pagination(**pagination_json) meta.pagination = pagination return meta def add_meta_to_result(result, json_content, attr_name): # type: (List[BoundModelBase], json, string) -> PageResult class_name = "PageResults{0}".format(attr_name.capitalize()) PageResults = namedtuple(class_name, [attr_name, "meta"]) return PageResults(**{attr_name: result, "meta": Meta.parse_meta(json_content)}) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/datacenters/0000755000175100001710000000000000000000000016170 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/datacenters/__init__.py0000644000175100001710000000003000000000000020272 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/datacenters/client.py0000644000175100001710000001007600000000000020024 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.datacenters.domain import Datacenter, DatacenterServerTypes from hcloud.locations.client import BoundLocation from hcloud.server_types.client import BoundServerType class BoundDatacenter(BoundModelBase): model = Datacenter def __init__(self, client, data): location = data.get("location") if location is not None: data["location"] = BoundLocation(client._client.locations, location) server_types = data.get("server_types") if server_types is not None: available = [ BoundServerType( client._client.server_types, {"id": server_type}, complete=False ) for server_type in server_types["available"] ] supported = [ BoundServerType( client._client.server_types, {"id": server_type}, complete=False ) for server_type in server_types["supported"] ] available_for_migration = [ BoundServerType( client._client.server_types, {"id": server_type}, complete=False ) for server_type in server_types["available_for_migration"] ] data["server_types"] = DatacenterServerTypes( available=available, supported=supported, available_for_migration=available_for_migration, ) super(BoundDatacenter, self).__init__(client, data) class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "datacenters" def get_by_id(self, id): # type: (int) -> BoundDatacenter """Get a specific datacenter by its ID. :param id: int :return: :class:`BoundDatacenter ` """ response = self._client.request( url="/datacenters/{datacenter_id}".format(datacenter_id=id), method="GET" ) return BoundDatacenter(self, response["datacenter"]) def get_list( self, name=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundDatacenter], Meta] """Get a list of datacenters :param name: str (optional) Can be used to filter datacenters by their name. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundDatacenter `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url="/datacenters", method="GET", params=params) datacenters = [ BoundDatacenter(self, datacenter_data) for datacenter_data in response["datacenters"] ] return self._add_meta_to_result(datacenters, response) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundDatacenter] """Get all datacenters :param name: str (optional) Can be used to filter datacenters by their name. :return: List[:class:`BoundDatacenter `] """ return super(DatacentersClient, self).get_all(name=name) def get_by_name(self, name): # type: (str) -> BoundDatacenter """Get datacenter by name :param name: str Used to get datacenter by name. :return: :class:`BoundDatacenter ` """ return super(DatacentersClient, self).get_by_name(name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/datacenters/domain.py0000644000175100001710000000333200000000000020012 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.domain import BaseDomain, DomainIdentityMixin class Datacenter(BaseDomain, DomainIdentityMixin): """Datacenter Domain :param id: int ID of Datacenter :param name: str Name of Datacenter :param description: str Description of Datacenter :param location: :class:`BoundLocation ` :param server_types: :class:`DatacenterServerTypes ` """ __slots__ = ( "id", "name", "description", "location", "server_types", ) def __init__( self, id=None, name=None, description=None, location=None, server_types=None ): self.id = id self.name = name self.description = description self.location = location self.server_types = server_types class DatacenterServerTypes: """DatacenterServerTypes Domain :param available: List[:class:`BoundServerTypes `] All available server types for this datacenter :param supported: List[:class:`BoundServerTypes `] All supported server types for this datacenter :param available_for_migration: List[:class:`BoundServerTypes `] All available for migration (change type) server types for this datacenter """ __slots__ = ("available", "supported", "available_for_migration") def __init__(self, available, supported, available_for_migration): self.available = available self.supported = supported self.available_for_migration = available_for_migration ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/firewalls/0000755000175100001710000000000000000000000015663 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/firewalls/__init__.py0000644000175100001710000000003000000000000017765 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/firewalls/client.py0000644000175100001710000004455400000000000017527 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result from hcloud.firewalls.domain import ( Firewall, CreateFirewallResponse, FirewallRule, FirewallResource, FirewallResourceLabelSelector, ) class BoundFirewall(BoundModelBase): model = Firewall def __init__(self, client, data, complete=True): rules = data.get("rules", []) if rules: rules = [ FirewallRule( direction=rule["direction"], source_ips=rule["source_ips"], destination_ips=rule["destination_ips"], protocol=rule["protocol"], port=rule["port"], description=rule["description"], ) for rule in rules ] data["rules"] = rules applied_to = data.get("applied_to", []) if applied_to: from hcloud.servers.client import BoundServer ats = [] for a in applied_to: if a["type"] == FirewallResource.TYPE_SERVER: ats.append( FirewallResource( type=a["type"], server=BoundServer( client._client.servers, a["server"], complete=False ), ) ) elif a["type"] == FirewallResource.TYPE_LABEL_SELECTOR: ats.append( FirewallResource( type=a["type"], label_selector=FirewallResourceLabelSelector( selector=a["label_selector"]["selector"] ), ) ) data["applied_to"] = ats super(BoundFirewall, self).__init__(client, data, complete) def get_actions_list(self, status=None, sort=None, page=None, per_page=None): # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResult[BoundAction, Meta] """Returns all action objects for a Firewall. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): # type: (Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a Firewall. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) def update(self, name=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFirewall """Updates the name or labels of a Firewall. :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str (optional) New Name to set :return: :class:`BoundFirewall ` """ return self._client.update(self, labels, name) def delete(self): # type: () -> bool """Deletes a Firewall. :return: boolean """ return self._client.delete(self) def set_rules(self, rules): # type: (List[FirewallRule]) -> List[BoundAction] """Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules. :param rules: List[:class:`FirewallRule `] :return: List[:class:`BoundAction `] """ return self._client.set_rules(self, rules) def apply_to_resources(self, resources): # type: (List[FirewallResource]) -> List[BoundAction] """Applies one Firewall to multiple resources. :param resources: List[:class:`FirewallResource `] :return: List[:class:`BoundAction `] """ return self._client.apply_to_resources(self, resources) def remove_from_resources(self, resources): # type: (List[FirewallResource]) -> List[BoundAction] """Removes one Firewall from multiple resources. :param resources: List[:class:`FirewallResource `] :return: List[:class:`BoundAction `] """ return self._client.remove_from_resources(self, resources) class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "firewalls" def get_actions_list( self, firewall, # type: Firewall status=None, # type: Optional[List[str]] sort=None, # type: Optional[List[str]] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundAction], Meta] """Returns all action objects for a Firewall. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ params = {} if status is not None: params["status"] = status if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/firewalls/{firewall_id}/actions".format(firewall_id=firewall.id), method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return add_meta_to_result(actions, response, "actions") def get_actions( self, firewall, # type: Firewall status=None, # type: Optional[List[str]] sort=None, # type: Optional[List[str]] ): # type: (...) -> List[BoundAction] """Returns all action objects for a Firewall. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return super(FirewallsClient, self).get_actions( firewall, status=status, sort=sort ) def get_by_id(self, id): # type: (int) -> BoundFirewall """Returns a specific Firewall object. :param id: int :return: :class:`BoundFirewall ` """ response = self._client.request( url="/firewalls/{firewall_id}".format(firewall_id=id), method="GET" ) return BoundFirewall(self, response["firewall"]) def get_list( self, label_selector=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] name=None, # type: Optional[str] sort=None, # type: Optional[List[str]] ): # type: (...) -> PageResults[List[BoundFirewall]] """Get a list of floating ips from this account :param label_selector: str (optional) Can be used to filter Firewalls by labels. The response will only contain Firewalls matching the label selector values. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :param name: str (optional) Can be used to filter networks by their name. :param sort: List[str] (optional) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: (List[:class:`BoundFirewall `], :class:`Meta `) """ params = {} if label_selector is not None: params["label_selector"] = label_selector if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page if name is not None: params["name"] = name if sort is not None: params["sort"] = sort response = self._client.request(url="/firewalls", method="GET", params=params) firewalls = [ BoundFirewall(self, firewall_data) for firewall_data in response["firewalls"] ] return self._add_meta_to_result(firewalls, response) def get_all(self, label_selector=None, name=None, sort=None): # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundFirewall] """Get all floating ips from this account :param label_selector: str (optional) Can be used to filter Firewalls by labels. The response will only contain Firewalls matching the label selector values. :param name: str (optional) Can be used to filter networks by their name. :param sort: List[str] (optional) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: List[:class:`BoundFirewall `] """ return super(FirewallsClient, self).get_all( label_selector=label_selector, name=name, sort=sort ) def get_by_name(self, name): # type: (str) -> BoundFirewall """Get Firewall by name :param name: str Used to get Firewall by name. :return: :class:`BoundFirewall ` """ return super(FirewallsClient, self).get_by_name(name) def create( self, name, # type: str rules=None, # type: Optional[List[FirewallRule]] labels=None, # type: Optional[str] resources=None, # type: Optional[List[FirewallResource]] ): # type: (...) -> CreateFirewallResponse """Creates a new Firewall. :param name: str Firewall Name :param rules: List[:class:`FirewallRule `] (optional) :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param resources: List[:class:`FirewallResource `] (optional) :return: :class:`CreateFirewallResponse ` """ data = {"name": name} if labels is not None: data["labels"] = labels if rules is not None: data.update({"rules": []}) for rule in rules: data["rules"].append(rule.to_payload()) if resources is not None: data.update({"apply_to": []}) for resource in resources: data["apply_to"].append(resource.to_payload()) response = self._client.request(url="/firewalls", json=data, method="POST") actions = [] if response.get("actions") is not None: actions = [ BoundAction(self._client.actions, _) for _ in response["actions"] ] result = CreateFirewallResponse( firewall=BoundFirewall(self, response["firewall"]), actions=actions ) return result def update(self, firewall, labels=None, name=None): # type: (Firewall, Optional[Dict[str, str]], Optional[str]) -> BoundFirewall """Updates the description or labels of a Firewall. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str (optional) New name to set :return: :class:`BoundFirewall ` """ data = {} if labels is not None: data["labels"] = labels if name is not None: data["name"] = name response = self._client.request( url="/firewalls/{firewall_id}".format(firewall_id=firewall.id), method="PUT", json=data, ) return BoundFirewall(self, response["firewall"]) def delete(self, firewall): # type: (Firewall) -> bool """Deletes a Firewall. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :return: boolean """ self._client.request( url="/firewalls/{firewall_id}".format(firewall_id=firewall.id), method="DELETE", ) # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised return True def set_rules(self, firewall, rules): # type: (Firewall, List[FirewallRule]) -> List[BoundAction] """Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :param rules: List[:class:`FirewallRule `] :return: List[:class:`BoundAction `] """ data = {"rules": []} for rule in rules: data["rules"].append(rule.to_payload()) response = self._client.request( url="/firewalls/{firewall_id}/actions/set_rules".format( firewall_id=firewall.id ), method="POST", json=data, ) return [BoundAction(self._client.actions, _) for _ in response["actions"]] def apply_to_resources(self, firewall, resources): # type: (Firewall, List[FirewallResource]) -> List[BoundAction] """Applies one Firewall to multiple resources. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :param resources: List[:class:`FirewallResource `] :return: List[:class:`BoundAction `] """ data = {"apply_to": []} for resource in resources: data["apply_to"].append(resource.to_payload()) response = self._client.request( url="/firewalls/{firewall_id}/actions/apply_to_resources".format( firewall_id=firewall.id ), method="POST", json=data, ) return [BoundAction(self._client.actions, _) for _ in response["actions"]] def remove_from_resources(self, firewall, resources): # type: (Firewall, List[FirewallResource]) -> List[BoundAction] """Removes one Firewall from multiple resources. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :param resources: List[:class:`FirewallResource `] :return: List[:class:`BoundAction `] """ data = {"remove_from": []} for resource in resources: data["remove_from"].append(resource.to_payload()) response = self._client.request( url="/firewalls/{firewall_id}/actions/remove_from_resources".format( firewall_id=firewall.id ), method="POST", json=data, ) return [BoundAction(self._client.actions, _) for _ in response["actions"]] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/firewalls/domain.py0000644000175100001710000001322200000000000017504 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain class Firewall(BaseDomain): """Firewall Domain :param id: int ID of the Firewall :param name: str Name of the Firewall :param labels: dict User-defined labels (key-value pairs) :param rules: List[:class:`FirewallRule `] Rules of the Firewall :param applied_to: List[:class:`FirewallResource `] Resources currently using the Firewall :param created: datetime Point in time when the image was created """ __slots__ = ("id", "name", "labels", "rules", "applied_to", "created") def __init__( self, id=None, name=None, labels=None, rules=None, applied_to=None, created=None ): self.id = id self.name = name self.rules = rules self.applied_to = applied_to self.labels = labels self.created = isoparse(created) if created else None class FirewallRule: """Firewall Rule Domain :param direction: str The Firewall which was created :param port: str Port to which traffic will be allowed, only applicable for protocols TCP and UDP, specify port ranges by using - as a indicator, Sample: 80-85 means all ports between 80 & 85 (80, 82, 83, 84, 85) :param protocol: str Select traffic direction on which rule should be applied. Use source_ips for direction in and destination_ips for direction out. :param source_ips: List[str] List of permitted IPv4/IPv6 addresses in CIDR notation. Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. You can specify 100 CIDRs at most. :param destination_ips: List[str] List of permitted IPv4/IPv6 addresses in CIDR notation. Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. You can specify 100 CIDRs at most. :param description: str Short description of the firewall rule """ __slots__ = ( "direction", "port", "protocol", "source_ips", "destination_ips", "description", ) DIRECTION_IN = "in" """Firewall Rule Direction In""" DIRECTION_OUT = "out" """Firewall Rule Direction Out""" PROTOCOL_UDP = "udp" """Firewall Rule Protocol UDP""" PROTOCOL_ICMP = "icmp" """Firewall Rule Protocol ICMP""" PROTOCOL_TCP = "tcp" """Firewall Rule Protocol TCP""" PROTOCOL_ESP = "esp" """Firewall Rule Protocol ESP""" PROTOCOL_GRE = "gre" """Firewall Rule Protocol GRE""" def __init__( self, direction, # type: str protocol, # type: str source_ips, # type: List[str] port=None, # type: Optional[str] destination_ips=None, # type: Optional[List[str]] description=None, # type: Optional[str] ): self.direction = direction self.port = port self.protocol = protocol self.source_ips = source_ips self.destination_ips = destination_ips or [] self.description = description def to_payload(self): payload = { "direction": self.direction, "protocol": self.protocol, "source_ips": self.source_ips, } if len(self.destination_ips) > 0: payload.update({"destination_ips": self.destination_ips}) if self.port is not None: payload.update({"port": self.port}) if self.description is not None: payload.update({"description": self.description}) return payload class FirewallResource: """Firewall Used By Domain :param type: str Type of resource referenced :param server: Optional[Server] Server the Firewall is applied to :param label_selector: Optional[FirewallResourceLabelSelector] Label Selector for Servers the Firewall should be applied to """ __slots__ = ("type", "server", "label_selector") TYPE_SERVER = "server" """Firewall Used By Type Server""" TYPE_LABEL_SELECTOR = "label_selector" """Firewall Used By Type label_selector""" def __init__( self, type, # type: str server=None, # type: Optional[Server] label_selector=None, # type: Optional[FirewallResourceLabelSelector] ): self.type = type self.server = server self.label_selector = label_selector def to_payload(self): payload = { "type": self.type, } if self.server is not None: payload.update({"server": {"id": self.server.id}}) if self.label_selector is not None: payload.update( {"label_selector": {"selector": self.label_selector.selector}} ) return payload class FirewallResourceLabelSelector(BaseDomain): """FirewallResourceLabelSelector Domain :param selector: str Target label selector """ def __init__(self, selector=None): self.selector = selector class CreateFirewallResponse(BaseDomain): """Create Firewall Response Domain :param firewall: :class:`BoundFirewall ` The Firewall which was created :param actions: List[:class:`BoundAction `] The Action which shows the progress of the Firewall Creation """ __slots__ = ("firewall", "actions") def __init__( self, firewall, # type: BoundFirewall actions, # type: BoundAction ): self.firewall = firewall self.actions = actions ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/floating_ips/0000755000175100001710000000000000000000000016351 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/floating_ips/__init__.py0000644000175100001710000000003000000000000020453 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/floating_ips/client.py0000644000175100001710000004611500000000000020210 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result from hcloud.floating_ips.domain import FloatingIP, CreateFloatingIPResponse from hcloud.locations.client import BoundLocation class BoundFloatingIP(BoundModelBase): model = FloatingIP def __init__(self, client, data, complete=True): from hcloud.servers.client import BoundServer server = data.get("server") if server is not None: data["server"] = BoundServer( client._client.servers, {"id": server}, complete=False ) home_location = data.get("home_location") if home_location is not None: data["home_location"] = BoundLocation( client._client.locations, home_location ) super(BoundFloatingIP, self).__init__(client, data, complete) def get_actions_list(self, status=None, sort=None, page=None, per_page=None): # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResult[BoundAction, Meta] """Returns all action objects for a Floating IP. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): # type: (Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a Floating IP. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) def update(self, description=None, labels=None, name=None): # type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFloatingIP """Updates the description or labels of a Floating IP. :param description: str (optional) New Description to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str (optional) New Name to set :return: :class:`BoundFloatingIP ` """ return self._client.update(self, description, labels, name) def delete(self): # type: () -> bool """Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned. :return: boolean """ return self._client.delete(self) def change_protection(self, delete=None): # type: (Optional[bool]) -> BoundAction """Changes the protection configuration of the Floating IP. :param delete: boolean If true, prevents the Floating IP from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete) def assign(self, server): # type: (Server) -> BoundAction """Assigns a Floating IP to a server. :param server: :class:`BoundServer ` or :class:`Server ` Server the Floating IP shall be assigned to :return: :class:`BoundAction ` """ return self._client.assign(self, server) def unassign(self): # type: () -> BoundAction """Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time. :return: :class:`BoundAction ` """ return self._client.unassign(self) def change_dns_ptr(self, ip, dns_ptr): # type: (str, str) -> BoundAction """Changes the hostname that will appear when getting the hostname belonging to this Floating IP. :param ip: str The IP address for which to set the reverse DNS entry :param dns_ptr: str Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` :return: :class:`BoundAction ` """ return self._client.change_dns_ptr(self, ip, dns_ptr) class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "floating_ips" def get_actions_list( self, floating_ip, # type: FloatingIP status=None, # type: Optional[List[str]] sort=None, # type: Optional[List[str]] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundAction], Meta] """Returns all action objects for a Floating IP. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ params = {} if status is not None: params["status"] = status if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/floating_ips/{floating_ip_id}/actions".format( floating_ip_id=floating_ip.id ), method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return add_meta_to_result(actions, response, "actions") def get_actions( self, floating_ip, # type: FloatingIP status=None, # type: Optional[List[str]] sort=None, # type: Optional[List[str]] ): # type: (...) -> List[BoundAction] """Returns all action objects for a Floating IP. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return super(FloatingIPsClient, self).get_actions( floating_ip, status=status, sort=sort ) def get_by_id(self, id): # type: (int) -> BoundFloatingIP """Returns a specific Floating IP object. :param id: int :return: :class:`BoundFloatingIP ` """ response = self._client.request( url="/floating_ips/{floating_ip_id}".format(floating_ip_id=id), method="GET" ) return BoundFloatingIP(self, response["floating_ip"]) def get_list( self, label_selector=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] name=None, # type: Optional[str] ): # type: (...) -> PageResults[List[BoundFloatingIP]] """Get a list of floating ips from this account :param label_selector: str (optional) Can be used to filter Floating IPs by labels. The response will only contain Floating IPs matching the label selector.able values. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :param name: str (optional) Can be used to filter networks by their name. :return: (List[:class:`BoundFloatingIP `], :class:`Meta `) """ params = {} if label_selector is not None: params["label_selector"] = label_selector if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page if name is not None: params["name"] = name response = self._client.request( url="/floating_ips", method="GET", params=params ) floating_ips = [ BoundFloatingIP(self, floating_ip_data) for floating_ip_data in response["floating_ips"] ] return self._add_meta_to_result(floating_ips, response) def get_all(self, label_selector=None, name=None): # type: (Optional[str], Optional[str]) -> List[BoundFloatingIP] """Get all floating ips from this account :param label_selector: str (optional) Can be used to filter Floating IPs by labels. The response will only contain Floating IPs matching the label selector.able values. :param name: str (optional) Can be used to filter networks by their name. :return: List[:class:`BoundFloatingIP `] """ return super(FloatingIPsClient, self).get_all( label_selector=label_selector, name=name ) def get_by_name(self, name): # type: (str) -> BoundFloatingIP """Get Floating IP by name :param name: str Used to get Floating IP by name. :return: :class:`BoundFloatingIP ` """ return super(FloatingIPsClient, self).get_by_name(name) def create( self, type, # type: str description=None, # type: Optional[str] labels=None, # type: Optional[str] home_location=None, # type: Optional[Location] server=None, # type: Optional[Server] name=None, # type: Optional[str] ): # type: (...) -> CreateFloatingIPResponse """Creates a new Floating IP assigned to a server. :param type: str Floating IP type Choices: ipv4, ipv6 :param description: str (optional) :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param home_location: :class:`BoundLocation ` or :class:`Location ` ( Home location (routing is optimized for that location). Only optional if server argument is passed. :param server: :class:`BoundServer ` or :class:`Server ` Server to assign the Floating IP to :param name: str (optional) :return: :class:`CreateFloatingIPResponse ` """ data = {"type": type} if description is not None: data["description"] = description if labels is not None: data["labels"] = labels if home_location is not None: data["home_location"] = home_location.id_or_name if server is not None: data["server"] = server.id if name is not None: data["name"] = name response = self._client.request(url="/floating_ips", json=data, method="POST") action = None if response.get("action") is not None: action = BoundAction(self._client.actions, response["action"]) result = CreateFloatingIPResponse( floating_ip=BoundFloatingIP(self, response["floating_ip"]), action=action ) return result def update(self, floating_ip, description=None, labels=None, name=None): # type: (FloatingIP, Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFloatingIP """Updates the description or labels of a Floating IP. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :param description: str (optional) New Description to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str (optional) New name to set :return: :class:`BoundFloatingIP ` """ data = {} if description is not None: data["description"] = description if labels is not None: data["labels"] = labels if name is not None: data["name"] = name response = self._client.request( url="/floating_ips/{floating_ip_id}".format(floating_ip_id=floating_ip.id), method="PUT", json=data, ) return BoundFloatingIP(self, response["floating_ip"]) def delete(self, floating_ip): # type: (FloatingIP) -> bool """Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :return: boolean """ self._client.request( url="/floating_ips/{floating_ip_id}".format(floating_ip_id=floating_ip.id), method="DELETE", ) # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised return True def change_protection(self, floating_ip, delete=None): # type: (FloatingIP, Optional[bool]) -> BoundAction """Changes the protection configuration of the Floating IP. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :param delete: boolean If true, prevents the Floating IP from being deleted :return: :class:`BoundAction ` """ data = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url="/floating_ips/{floating_ip_id}/actions/change_protection".format( floating_ip_id=floating_ip.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def assign(self, floating_ip, server): # type: (FloatingIP, Server) -> BoundAction """Assigns a Floating IP to a server. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :param server: :class:`BoundServer ` or :class:`Server ` Server the Floating IP shall be assigned to :return: :class:`BoundAction ` """ response = self._client.request( url="/floating_ips/{floating_ip_id}/actions/assign".format( floating_ip_id=floating_ip.id ), method="POST", json={"server": server.id}, ) return BoundAction(self._client.actions, response["action"]) def unassign(self, floating_ip): # type: (FloatingIP) -> BoundAction """Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :return: :class:`BoundAction ` """ response = self._client.request( url="/floating_ips/{floating_ip_id}/actions/unassign".format( floating_ip_id=floating_ip.id ), method="POST", ) return BoundAction(self._client.actions, response["action"]) def change_dns_ptr(self, floating_ip, ip, dns_ptr): # type: (FloatingIP, str, str) -> BoundAction """Changes the hostname that will appear when getting the hostname belonging to this Floating IP. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :param ip: str The IP address for which to set the reverse DNS entry :param dns_ptr: str Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` :return: :class:`BoundAction ` """ response = self._client.request( url="/floating_ips/{floating_ip_id}/actions/change_dns_ptr".format( floating_ip_id=floating_ip.id ), method="POST", json={"ip": ip, "dns_ptr": dns_ptr}, ) return BoundAction(self._client.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/floating_ips/domain.py0000644000175100001710000000533400000000000020177 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain class FloatingIP(BaseDomain): """Floating IP Domain :param id: int ID of the Floating IP :param description: str, None Description of the Floating IP :param ip: str IP address of the Floating IP :param type: str Type of Floating IP. Choices: `ipv4`, `ipv6` :param server: :class:`BoundServer `, None Server the Floating IP is assigned to, None if it is not assigned at all :param dns_ptr: List[Dict] Array of reverse DNS entries :param home_location: :class:`BoundLocation ` Location the Floating IP was created in. Routing is optimized for this location. :param blocked: boolean Whether the IP is blocked :param protection: dict Protection configuration for the Floating IP :param labels: dict User-defined labels (key-value pairs) :param created: datetime Point in time when the Floating IP was created :param name: str Name of the Floating IP """ __slots__ = ( "id", "type", "description", "ip", "server", "dns_ptr", "home_location", "blocked", "protection", "labels", "name", "created", ) def __init__( self, id=None, type=None, description=None, ip=None, server=None, dns_ptr=None, home_location=None, blocked=None, protection=None, labels=None, created=None, name=None, ): self.id = id self.type = type self.description = description self.ip = ip self.server = server self.dns_ptr = dns_ptr self.home_location = home_location self.blocked = blocked self.protection = protection self.labels = labels self.created = isoparse(created) if created else None self.name = name class CreateFloatingIPResponse(BaseDomain): """Create Floating IP Response Domain :param floating_ip: :class:`BoundFloatingIP ` The Floating IP which was created :param action: :class:`BoundAction ` The Action which shows the progress of the Floating IP Creation """ __slots__ = ("floating_ip", "action") def __init__( self, floating_ip, # type: BoundFloatingIP action, # type: BoundAction ): self.floating_ip = floating_ip self.action = action ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/hcloud.py0000644000175100001710000002056700000000000015535 0ustar00runnerdocker# -*- coding: utf-8 -*- from __future__ import absolute_import import time import requests from hcloud.actions.client import ActionsClient from hcloud.certificates.client import CertificatesClient from hcloud.floating_ips.client import FloatingIPsClient from hcloud.networks.client import NetworksClient from hcloud.isos.client import IsosClient from hcloud.servers.client import ServersClient from hcloud.server_types.client import ServerTypesClient from hcloud.ssh_keys.client import SSHKeysClient from hcloud.volumes.client import VolumesClient from hcloud.images.client import ImagesClient from hcloud.locations.client import LocationsClient from hcloud.datacenters.client import DatacentersClient from hcloud.load_balancers.client import LoadBalancersClient from hcloud.load_balancer_types.client import LoadBalancerTypesClient from hcloud.placement_groups.client import PlacementGroupsClient from .__version__ import VERSION from .firewalls.client import FirewallsClient class APIException(Exception): """There was an error while performing an API Request""" def __init__(self, code, message, details): self.code = code self.message = message self.details = details def __str__(self): return self.message class Client(object): """Base Client for accessing the Hetzner Cloud API""" _version = VERSION _retry_wait_time = 0.5 __user_agent_prefix = "hcloud-python" def __init__( self, token, api_endpoint="https://api.hetzner.cloud/v1", application_name=None, application_version=None, poll_interval=1, ): """Create an new Client instance :param token: str Hetzner Cloud API token :param api_endpoint: str Hetzner Cloud API endpoint (default is https://api.hetzner.cloud/v1) :param application_name: str Your application name (default is None) :param application_version: str Your application _version (default is None) :param poll_interval: int Interval for polling information from Hetzner Cloud API in seconds (default is 1) """ self.token = token self._api_endpoint = api_endpoint self._application_name = application_name self._application_version = application_version self._requests_session = requests.Session() self.poll_interval = poll_interval self.datacenters = DatacentersClient(self) """DatacentersClient Instance :type: :class:`DatacentersClient ` """ self.locations = LocationsClient(self) """LocationsClient Instance :type: :class:`LocationsClient ` """ self.servers = ServersClient(self) """ServersClient Instance :type: :class:`ServersClient ` """ self.server_types = ServerTypesClient(self) """ServerTypesClient Instance :type: :class:`ServerTypesClient ` """ self.volumes = VolumesClient(self) """VolumesClient Instance :type: :class:`VolumesClient ` """ self.actions = ActionsClient(self) """ActionsClient Instance :type: :class:`ActionsClient ` """ self.images = ImagesClient(self) """ImagesClient Instance :type: :class:`ImagesClient ` """ self.isos = IsosClient(self) """ImagesClient Instance :type: :class:`IsosClient ` """ self.ssh_keys = SSHKeysClient(self) """SSHKeysClient Instance :type: :class:`SSHKeysClient ` """ self.floating_ips = FloatingIPsClient(self) """FloatingIPsClient Instance :type: :class:`FloatingIPsClient ` """ self.networks = NetworksClient(self) """NetworksClient Instance :type: :class:`NetworksClient ` """ self.certificates = CertificatesClient(self) """CertificatesClient Instance :type: :class:`CertificatesClient ` """ self.load_balancers = LoadBalancersClient(self) """LoadBalancersClient Instance :type: :class:`LoadBalancersClient ` """ self.load_balancer_types = LoadBalancerTypesClient(self) """LoadBalancerTypesClient Instance :type: :class:`LoadBalancerTypesClient ` """ self.firewalls = FirewallsClient(self) """FirewallsClient Instance :type: :class:`FirewallsClient ` """ self.placement_groups = PlacementGroupsClient(self) """PlacementGroupsClient Instance :type: :class:`PlacementGroupsClient ` """ def _get_user_agent(self): """Get the user agent of the hcloud-python instance with the user application name (if specified) :return: str The user agent of this hcloud-python instance """ if self._application_name is not None and self._application_version is None: return "{application_name} {prefix}/{version}".format( application_name=self._application_name, prefix=self.__user_agent_prefix, version=self._version, ) elif ( self._application_name is not None and self._application_version is not None ): return "{application_name}/{application_version} {prefix}/{version}".format( application_name=self._application_name, application_version=self._application_version, prefix=self.__user_agent_prefix, version=self._version, ) else: return "{prefix}/{version}".format( prefix=self.__user_agent_prefix, version=self._version ) def _get_headers(self): headers = { "User-Agent": self._get_user_agent(), "Authorization": "Bearer {token}".format(token=self.token), } return headers def _raise_exception_from_response(self, response): raise APIException( code=response.status_code, message=response.reason, details={"content": response.content}, ) def _raise_exception_from_json_content(self, json_content): raise APIException( code=json_content["error"]["code"], message=json_content["error"]["message"], details=json_content["error"]["details"], ) def request(self, method, url, tries=1, **kwargs): """Perform a request to the Hetzner Cloud API, wrapper around requests.request :param method: str HTTP Method to perform the Request :param url: str URL of the Endpoint :param tries: int Tries of the request (used internally, should not be set by the user) :return: Response :rtype: requests.Response """ response = self._requests_session.request( method, self._api_endpoint + url, headers=self._get_headers(), **kwargs ) json_content = response.content try: if len(json_content) > 0: json_content = response.json() except (TypeError, ValueError): self._raise_exception_from_response(response) if not response.ok: if json_content: if json_content["error"]["code"] == "rate_limit_exceeded" and tries < 5: time.sleep(tries * self._retry_wait_time) tries = tries + 1 return self.request(method, url, tries, **kwargs) else: self._raise_exception_from_json_content(json_content) else: self._raise_exception_from_response(response) return json_content ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/helpers/0000755000175100001710000000000000000000000015335 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/helpers/__init__.py0000644000175100001710000000003000000000000017437 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/images/0000755000175100001710000000000000000000000015140 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/images/__init__.py0000644000175100001710000000003000000000000017242 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/images/client.py0000644000175100001710000003550700000000000017002 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result from hcloud.images.domain import Image class BoundImage(BoundModelBase): model = Image def __init__(self, client, data): from hcloud.servers.client import BoundServer created_from = data.get("created_from") if created_from is not None: data["created_from"] = BoundServer( client._client.servers, created_from, complete=False ) bound_to = data.get("bound_to") if bound_to is not None: data["bound_to"] = BoundServer( client._client.servers, {"id": bound_to}, complete=False ) super(BoundImage, self).__init__(client, data) def get_actions_list(self, sort=None, page=None, per_page=None, status=None): # type: (Optional[List[str]], Optional[int], Optional[int], Optional[List[str]]) -> PageResult[BoundAction, Meta] """Returns a list of action objects for the image. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list( self, sort=sort, page=page, per_page=per_page, status=status ) def get_actions(self, sort=None, status=None): # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for the image. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status=status, sort=sort) def update(self, description=None, type=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]]) -> BoundImage """Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels. :param description: str (optional) New description of Image :param type: str (optional) Destination image type to convert to Choices: snapshot :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundImage ` """ return self._client.update(self, description, type, labels) def delete(self): # type: () -> bool """Deletes an Image. Only images of type snapshot and backup can be deleted. :return: bool """ return self._client.delete(self) def change_protection(self, delete=None): # type: (Optional[bool]) -> BoundAction """Changes the protection configuration of the image. Can only be used on snapshots. :param delete: bool If true, prevents the snapshot from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete) class ImagesClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "images" def get_actions_list( self, image, # type: Image sort=None, # type: Optional[List[str]] page=None, # type: Optional[int] per_page=None, # type: Optional[int] status=None, # type: Optional[List[str]] ): # type: (...) -> PageResults[List[BoundAction], Meta] """Returns a list of action objects for an image. :param image: :class:`BoundImage ` or :class:`Image ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ params = {} if sort is not None: params["sort"] = sort if status is not None: params["status"] = status if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/images/{image_id}/actions".format(image_id=image.id), method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return add_meta_to_result(actions, response, "actions") def get_actions( self, image, # type: Image sort=None, # type: Optional[List[str]] status=None, # type: Optional[List[str]] ): # type: (...) -> List[BoundAction] """Returns all action objects for an image. :param image: :class:`BoundImage ` or :class:`Image ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) :return: List[:class:`BoundAction `] """ return super(ImagesClient, self).get_actions(image, sort=sort, status=status) def get_by_id(self, id): # type: (int) -> BoundImage """Get a specific Image :param id: int :return: :class:`BoundImage PageResults[List[BoundImage]] """Get all images :param name: str (optional) Can be used to filter images by their name. :param label_selector: str (optional) Can be used to filter servers by labels. The response will only contain servers matching the label selector. :param bound_to: List[str] (optional) Server Id linked to the image. Only available for images of type backup :param type: List[str] (optional) Choices: system snapshot backup :param status: List[str] (optional) Can be used to filter images by their status. The response will only contain images matching the status. :param sort: List[str] (optional) Choices: id id:asc id:desc name name:asc name:desc created created:asc created:desc :param include_deprecated: bool (optional) Include deprecated images in the response. Default: False :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundImage `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if label_selector is not None: params["label_selector"] = label_selector if bound_to is not None: params["bound_to"] = bound_to if type is not None: params["type"] = type if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page if status is not None: params["status"] = per_page if include_deprecated is not None: params["include_deprecated"] = include_deprecated response = self._client.request(url="/images", method="GET", params=params) images = [BoundImage(self, image_data) for image_data in response["images"]] return self._add_meta_to_result(images, response) def get_all( self, name=None, # type: Optional[str] label_selector=None, # type: Optional[str] bound_to=None, # type: Optional[List[str]] type=None, # type: Optional[List[str]] sort=None, # type: Optional[List[str]] status=None, # type: Optional[List[str]] include_deprecated=None, # type: Optional[bool] ): # type: (...) -> List[BoundImage] """Get all images :param name: str (optional) Can be used to filter images by their name. :param label_selector: str (optional) Can be used to filter servers by labels. The response will only contain servers matching the label selector. :param bound_to: List[str] (optional) Server Id linked to the image. Only available for images of type backup :param type: List[str] (optional) Choices: system snapshot backup :param status: List[str] (optional) Can be used to filter images by their status. The response will only contain images matching the status. :param sort: List[str] (optional) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :param include_deprecated: bool (optional) Include deprecated images in the response. Default: False :return: List[:class:`BoundImage `] """ return super(ImagesClient, self).get_all( name=name, label_selector=label_selector, bound_to=bound_to, type=type, sort=sort, status=status, include_deprecated=include_deprecated, ) def get_by_name(self, name): # type: (str) -> BoundImage """Get image by name :param name: str Used to get image by name. :return: :class:`BoundImage ` """ return super(ImagesClient, self).get_by_name(name) def update(self, image, description=None, type=None, labels=None): # type:(Image, Optional[str], Optional[str], Optional[Dict[str, str]]) -> BoundImage """Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels. :param image: :class:`BoundImage ` or :class:`Image ` :param description: str (optional) New description of Image :param type: str (optional) Destination image type to convert to Choices: snapshot :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundImage ` """ data = {} if description is not None: data.update({"description": description}) if type is not None: data.update({"type": type}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url="/images/{image_id}".format(image_id=image.id), method="PUT", json=data ) return BoundImage(self, response["image"]) def delete(self, image): # type: (Image) -> bool """Deletes an Image. Only images of type snapshot and backup can be deleted. :param :class:`BoundImage ` or :class:`Image ` :return: bool """ self._client.request( url="/images/{image_id}".format(image_id=image.id), method="DELETE" ) # Return allays true, because the API does not return an action for it. When an error occurs a APIException will be raised return True def change_protection(self, image, delete=None): # type: (Image, Optional[bool], Optional[bool]) -> BoundAction """Changes the protection configuration of the image. Can only be used on snapshots. :param image: :class:`BoundImage ` or :class:`Image ` :param delete: bool If true, prevents the snapshot from being deleted :return: :class:`BoundAction ` """ data = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url="/images/{image_id}/actions/change_protection".format( image_id=image.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/images/domain.py0000644000175100001710000000735200000000000016770 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin class Image(BaseDomain, DomainIdentityMixin): """Image Domain :param id: int ID of the image :param type: str Type of the image Choices: `system`, `snapshot`, `backup`, `app` :param status: str Whether the image can be used or if it’s still being created Choices: `available`, `creating` :param name: str, None Unique identifier of the image. This value is only set for system images. :param description: str Description of the image :param image_size: number, None Size of the image file in our storage in GB. For snapshot images this is the value relevant for calculating costs for the image. :param disk_size: number Size of the disk contained in the image in GB. :param created: datetime Point in time when the image was created :param created_from: :class:`BoundServer `, None Information about the server the image was created from :param bound_to: :class:`BoundServer `, None ID of server the image is bound to. Only set for images of type `backup`. :param os_flavor: str Flavor of operating system contained in the image Choices: `ubuntu`, `centos`, `debian`, `fedora`, `unknown` :param os_version: str, None Operating system version :param rapid_deploy: bool Indicates that rapid deploy of the image is available :param protection: dict Protection configuration for the image :param deprecated: datetime, None Point in time when the image is considered to be deprecated (in ISO-8601 format) :param labels: Dict User-defined labels (key-value pairs) """ __slots__ = ( "id", "name", "type", "description", "image_size", "disk_size", "bound_to", "os_flavor", "os_version", "rapid_deploy", "created_from", "status", "protection", "labels", "created", "deprecated", ) def __init__( self, id=None, name=None, type=None, created=None, description=None, image_size=None, disk_size=None, deprecated=None, bound_to=None, os_flavor=None, os_version=None, rapid_deploy=None, created_from=None, protection=None, labels=None, status=None, ): self.id = id self.name = name self.type = type self.created = isoparse(created) if created else None self.description = description self.image_size = image_size self.disk_size = disk_size self.deprecated = isoparse(deprecated) if deprecated else None self.bound_to = bound_to self.os_flavor = os_flavor self.os_version = os_version self.rapid_deploy = rapid_deploy self.created_from = created_from self.protection = protection self.labels = labels self.status = status class CreateImageResponse(BaseDomain): """Create Image Response Domain :param image: :class:`BoundImage ` The Image which was created :param action: :class:`BoundAction ` The Action which shows the progress of the Floating IP Creation """ __slots__ = ("action", "image") def __init__( self, action, # type: BoundAction image, # type: BoundImage ): self.action = action self.image = image ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/isos/0000755000175100001710000000000000000000000014650 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/isos/__init__.py0000644000175100001710000000003000000000000016752 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/isos/client.py0000644000175100001710000000454000000000000016503 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.isos.domain import Iso class BoundIso(BoundModelBase): model = Iso class IsosClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "isos" def get_by_id(self, id): # type: (int) -> BoundIso """Get a specific ISO by its id :param id: int :return: :class:`BoundIso ` """ response = self._client.request( url="/isos/{iso_id}".format(iso_id=id), method="GET" ) return BoundIso(self, response["iso"]) def get_list( self, name=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundIso], Meta] """Get a list of ISOs :param name: str (optional) Can be used to filter ISOs by their name. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundIso `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url="/isos", method="GET", params=params) isos = [BoundIso(self, iso_data) for iso_data in response["isos"]] return self._add_meta_to_result(isos, response) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundIso] """Get all ISOs :param name: str (optional) Can be used to filter ISOs by their name. :return: List[:class:`BoundIso `] """ return super(IsosClient, self).get_all(name=name) def get_by_name(self, name): # type: (str) -> BoundIso """Get iso by name :param name: str Used to get iso by name. :return: :class:`BoundIso ` """ return super(IsosClient, self).get_by_name(name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/isos/domain.py0000644000175100001710000000206200000000000016471 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin class Iso(BaseDomain, DomainIdentityMixin): """Iso Domain :param id: int ID of the ISO :param name: str, None Unique identifier of the ISO. Only set for public ISOs :param description: str Description of the ISO :param type: str Type of the ISO. Choices: `public`, `private` :param deprecated: datetime, None ISO 8601 timestamp of deprecation, None if ISO is still available. After the deprecation time it will no longer be possible to attach the ISO to servers. """ __slots__ = ("id", "name", "type", "description", "deprecated") def __init__( self, id=None, name=None, type=None, description=None, deprecated=None, ): self.id = id self.name = name self.type = type self.description = description self.deprecated = isoparse(deprecated) if deprecated else None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/load_balancer_types/0000755000175100001710000000000000000000000017665 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/load_balancer_types/__init__.py0000644000175100001710000000003000000000000021767 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/load_balancer_types/client.py0000644000175100001710000000572600000000000021527 0ustar00runnerdockerfrom hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.load_balancer_types.domain import LoadBalancerType class BoundLoadBalancerType(BoundModelBase): model = LoadBalancerType class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "load_balancer_types" def get_by_id(self, id): # type: (int) -> load_balancer_types.client.BoundLoadBalancerType """Returns a specific Load Balancer Type. :param id: int :return: :class:`BoundLoadBalancerType ` """ response = self._client.request( url="/load_balancer_types/{load_balancer_type_id}".format( load_balancer_type_id=id ), method="GET", ) return BoundLoadBalancerType(self, response["load_balancer_type"]) def get_list(self, name=None, page=None, per_page=None): # type: (Optional[str], Optional[int], Optional[int]) -> PageResults[List[BoundLoadBalancerType], Meta] """Get a list of Load Balancer types :param name: str (optional) Can be used to filter Load Balancer type by their name. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundLoadBalancerType `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/load_balancer_types", method="GET", params=params ) load_balancer_types = [ BoundLoadBalancerType(self, load_balancer_type_data) for load_balancer_type_data in response["load_balancer_types"] ] return self._add_meta_to_result(load_balancer_types, response) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundLoadBalancerType] """Get all Load Balancer types :param name: str (optional) Can be used to filter Load Balancer type by their name. :return: List[:class:`BoundLoadBalancerType `] """ return super(LoadBalancerTypesClient, self).get_all(name=name) def get_by_name(self, name): # type: (str) -> BoundLoadBalancerType """Get Load Balancer type by name :param name: str Used to get Load Balancer type by name. :return: :class:`BoundLoadBalancerType ` """ return super(LoadBalancerTypesClient, self).get_by_name(name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/load_balancer_types/domain.py0000644000175100001710000000317000000000000021507 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.domain import BaseDomain, DomainIdentityMixin class LoadBalancerType(BaseDomain, DomainIdentityMixin): """LoadBalancerType Domain :param id: int ID of the Load Balancer type :param name: str Name of the Load Balancer type :param description: str Description of the Load Balancer type :param max_connections: int Max amount of connections the Load Balancer can handle :param max_services: int Max amount of services the Load Balancer can handle :param max_targets: int Max amount of targets the Load Balancer can handle :param max_assigned_certificates: int Max amount of certificates the Load Balancer can serve :param prices: Dict Prices in different locations """ __slots__ = ( "id", "name", "description", "max_connections", "max_services", "max_targets", "max_assigned_certificates", "prices", ) def __init__( self, id=None, name=None, description=None, max_connections=None, max_services=None, max_targets=None, max_assigned_certificates=None, prices=None, ): self.id = id self.name = name self.description = description self.max_connections = max_connections self.max_services = max_services self.max_targets = max_targets self.max_assigned_certificates = max_assigned_certificates self.prices = prices ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/load_balancers/0000755000175100001710000000000000000000000016624 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/load_balancers/__init__.py0000644000175100001710000000000000000000000020723 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/load_balancers/client.py0000644000175100001710000012425500000000000020465 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.certificates.client import BoundCertificate from hcloud.servers.client import BoundServer from hcloud.load_balancer_types.client import BoundLoadBalancerType from hcloud.locations.client import BoundLocation from hcloud.networks.client import BoundNetwork from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result from hcloud.actions.client import BoundAction from hcloud.load_balancers.domain import ( LoadBalancer, IPv4Address, IPv6Network, PublicNetwork, PrivateNet, CreateLoadBalancerResponse, LoadBalancerTarget, LoadBalancerService, LoadBalancerServiceHttp, LoadBalancerHealthCheck, LoadBalancerHealtCheckHttp, LoadBalancerAlgorithm, LoadBalancerTargetLabelSelector, LoadBalancerTargetIP, ) class BoundLoadBalancer(BoundModelBase): model = LoadBalancer def __init__(self, client, data, complete=True): algorithm = data.get("algorithm") if algorithm: data["algorithm"] = LoadBalancerAlgorithm(type=algorithm["type"]) public_net = data.get("public_net") if public_net: ipv4_address = IPv4Address.from_dict(public_net["ipv4"]) ipv6_network = IPv6Network.from_dict(public_net["ipv6"]) data["public_net"] = PublicNetwork( ipv4=ipv4_address, ipv6=ipv6_network, enabled=public_net["enabled"] ) private_nets = data.get("private_net") if private_nets: private_nets = [ PrivateNet( network=BoundNetwork( client._client.networks, {"id": private_net["network"]}, complete=False, ), ip=private_net["ip"], ) for private_net in private_nets ] data["private_net"] = private_nets targets = data.get("targets") if targets: tmp_targets = [] for target in targets: tmp_target = LoadBalancerTarget(type=target["type"]) if target["type"] == "server": tmp_target.server = BoundServer( client._client.servers, data=target["server"], complete=False ) tmp_target.use_private_ip = target["use_private_ip"] elif target["type"] == "label_selector": tmp_target.label_selector = LoadBalancerTargetLabelSelector( selector=target["label_selector"]["selector"] ) tmp_target.use_private_ip = target["use_private_ip"] elif target["type"] == "ip": tmp_target.ip = LoadBalancerTargetIP(ip=target["ip"]["ip"]) tmp_targets.append(tmp_target) data["targets"] = tmp_targets services = data.get("services") if services: tmp_services = [] for service in services: tmp_service = LoadBalancerService( protocol=service["protocol"], listen_port=service["listen_port"], destination_port=service["destination_port"], proxyprotocol=service["proxyprotocol"], ) if service["protocol"] != "tcp": tmp_service.http = LoadBalancerServiceHttp( sticky_sessions=service["http"]["sticky_sessions"], redirect_http=service["http"]["redirect_http"], cookie_name=service["http"]["cookie_name"], cookie_lifetime=service["http"]["cookie_lifetime"], ) tmp_service.http.certificates = [ BoundCertificate( client._client.certificates, {"id": certificate}, complete=False, ) for certificate in service["http"]["certificates"] ] tmp_service.health_check = LoadBalancerHealthCheck( protocol=service["health_check"]["protocol"], port=service["health_check"]["port"], interval=service["health_check"]["interval"], retries=service["health_check"]["retries"], timeout=service["health_check"]["timeout"], ) if tmp_service.health_check.protocol != "tcp": tmp_service.health_check.http = LoadBalancerHealtCheckHttp( domain=service["health_check"]["http"]["domain"], path=service["health_check"]["http"]["path"], response=service["health_check"]["http"]["response"], tls=service["health_check"]["http"]["tls"], status_codes=service["health_check"]["http"]["status_codes"], ) tmp_services.append(tmp_service) data["services"] = tmp_services load_balancer_type = data.get("load_balancer_type") if load_balancer_type is not None: data["load_balancer_type"] = BoundLoadBalancerType( client._client.load_balancer_types, load_balancer_type ) location = data.get("location") if location is not None: data["location"] = BoundLocation(client._client.locations, location) super(BoundLoadBalancer, self).__init__(client, data, complete) def update(self, name=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]]) -> BoundLoadBalancer """Updates a Load Balancer. You can update a Load Balancers name and a Load Balancers labels. :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundLoadBalancer ` """ return self._client.update(self, name, labels) def delete(self): # type: () -> BoundAction """Deletes a Load Balancer. :return: boolean """ return self._client.delete(self) def get_actions_list(self, status=None, sort=None, page=None, per_page=None): # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] """Returns all action objects for a Load Balancer. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a Load Balancer. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) def add_service(self, service): # type: (LoadBalancerService) -> List[BoundAction] """Adds a service to a Load Balancer. :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to add to the Load Balancer :return: :class:`BoundAction ` """ return self._client.add_service(self, service=service) def update_service(self, service): # type: (LoadBalancerService) -> List[BoundAction] """Updates a service of an Load Balancer. :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to update :return: :class:`BoundAction ` """ return self._client.update_service(self, service=service) def delete_service(self, service): # type: (LoadBalancerService) -> List[BoundAction] """Deletes a service from a Load Balancer. :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to delete from the Load Balancer :return: :class:`BoundAction ` """ return self._client.delete_service(self, service) def add_target(self, target): # type: (LoadBalancerTarget) -> List[BoundAction] """Adds a target to a Load Balancer. :param target: :class:`LoadBalancerTarget ` The LoadBalancerTarget you want to add to the Load Balancer :return: :class:`BoundAction ` """ return self._client.add_target(self, target) def remove_target(self, target): # type: (LoadBalancerTarget) -> List[BoundAction] """Removes a target from a Load Balancer. :param target: :class:`LoadBalancerTarget ` The LoadBalancerTarget you want to remove from the Load Balancer :return: :class:`BoundAction ` """ return self._client.remove_target(self, target) def change_algorithm(self, algorithm): # type: (LoadBalancerAlgorithm) -> List[BoundAction] """Changes the algorithm used by the Load Balancer :param algorithm: :class:`LoadBalancerAlgorithm ` The LoadBalancerAlgorithm you want to use :return: :class:`BoundAction ` """ return self._client.change_algorithm(self, algorithm) def change_dns_ptr(self, ip, dns_ptr): # type: (str, str) -> BoundAction """Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer. :param ip: str The IP address for which to set the reverse DNS entry :param dns_ptr: str Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` :return: :class:`BoundAction ` """ return self._client.change_dns_ptr(self, ip, dns_ptr) def change_protection(self, delete): # type: (LoadBalancerService) -> List[BoundAction] """Changes the protection configuration of a Load Balancer. :param delete: boolean If True, prevents the Load Balancer from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete) def attach_to_network(self, network, ip=None): # type: (Union[Network,BoundNetwork],Optional[str]) -> BoundAction """Attaches a Load Balancer to a Network :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this Load Balancer :return: :class:`BoundAction ` """ return self._client.attach_to_network(self, network, ip) def detach_from_network(self, network): # type: ( Union[Network,BoundNetwork]) -> BoundAction """Detaches a Load Balancer from a Network. :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`BoundAction ` """ return self._client.detach_from_network(self, network) def enable_public_interface(self): # type: () -> BoundAction """Enables the public interface of a Load Balancer. :return: :class:`BoundAction ` """ return self._client.enable_public_interface(self) def disable_public_interface(self): # type: () -> BoundAction """Disables the public interface of a Load Balancer. :return: :class:`BoundAction ` """ return self._client.disable_public_interface(self) def change_type(self, load_balancer_type): # type: (Union[LoadBalancerType,BoundLoadBalancerType]) -> BoundAction """Changes the type of a Load Balancer. :param load_balancer_type: :class:`BoundLoadBalancerType ` or :class:`LoadBalancerType ` Load Balancer type the Load Balancer should migrate to :return: :class:`BoundAction ` """ return self._client.change_type(self, load_balancer_type) class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "load_balancers" def get_by_id(self, id): # type: (int) -> BoundLoadBalancer """Get a specific Load Balancer :param id: int :return: :class:`BoundLoadBalancer ` """ response = self._client.request( url="/load_balancers/{load_balancer_id}".format(load_balancer_id=id), method="GET", ) return BoundLoadBalancer(self, response["load_balancer"]) def get_list( self, name=None, # type: Optional[str] label_selector=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundLoadBalancer], Meta] """Get a list of Load Balancers from this account :param name: str (optional) Can be used to filter Load Balancers by their name. :param label_selector: str (optional) Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundLoadBalancer `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if label_selector is not None: params["label_selector"] = label_selector if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/load_balancers", method="GET", params=params ) ass_load_balancers = [ BoundLoadBalancer(self, load_balancer_data) for load_balancer_data in response["load_balancers"] ] return self._add_meta_to_result(ass_load_balancers, response) def get_all(self, name=None, label_selector=None): # type: (Optional[str], Optional[str]) -> List[BoundLoadBalancer] """Get all Load Balancers from this account :param name: str (optional) Can be used to filter Load Balancers by their name. :param label_selector: str (optional) Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. :return: List[:class:`BoundLoadBalancer `] """ return super(LoadBalancersClient, self).get_all( name=name, label_selector=label_selector ) def get_by_name(self, name): # type: (str) -> BoundLoadBalancer """Get Load Balancer by name :param name: str Used to get Load Balancer by name. :return: :class:`BoundLoadBalancer ` """ return super(LoadBalancersClient, self).get_by_name(name) def create( self, name, # type: str load_balancer_type, # type: LoadBalancerType algorithm=None, # type: Optional[LoadBalancerAlgorithm] services=None, # type: Optional[List[LoadBalancerService]] targets=None, # type: Optional[List[LoadBalancerTarget]] labels=None, # type: Optional[Dict[str, str]] location=None, # type: Optional[Location] network_zone=None, # type: Optional[str] public_interface=None, # type: Optional[bool] network=None, # type: Optional[Union[Network,BoundNetwork]] ): # type: (...) -> CreateLoadBalancerResponse: """Creates a Load Balancer . :param name: str Name of the Load Balancer :param load_balancer_type: LoadBalancerType Type of the Load Balancer :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param location: Location Location of the Load Balancer :param network_zone: str Network Zone of the Load Balancer :param algorithm: LoadBalancerAlgorithm (optional) The algorithm the Load Balancer is currently using :param services: LoadBalancerService The services the Load Balancer is currently serving :param targets: LoadBalancerTarget The targets the Load Balancer is currently serving :param public_interface: bool Enable or disable the public interface of the Load Balancer :param network: Network Adds the Load Balancer to a Network :return: :class:`CreateLoadBalancerResponse ` """ data = {"name": name, "load_balancer_type": load_balancer_type.id_or_name} if network is not None: data["network"] = network.id if public_interface is not None: data["public_interface"] = public_interface if labels is not None: data["labels"] = labels if algorithm is not None: data["algorithm"] = {"type": algorithm.type} if services is not None: service_list = [] for service in services: service_list.append(self.get_service_parameters(service)) data["services"] = service_list if targets is not None: target_list = [] for target in targets: target_data = { "type": target.type, "use_private_ip": target.use_private_ip, } if target.type == "server": target_data["server"] = {"id": target.server.id} elif target.type == "label_selector": target_data["label_selector"] = { "selector": target.label_selector.selector } elif target.type == "ip": target_data["ip"] = {"ip": target.ip.ip} target_list.append(target_data) data["targets"] = target_list if network_zone is not None: data["network_zone"] = network_zone if location is not None: data["location"] = location.id_or_name response = self._client.request(url="/load_balancers", method="POST", json=data) return CreateLoadBalancerResponse( load_balancer=BoundLoadBalancer(self, response["load_balancer"]), action=BoundAction(self._client.actions, response["action"]), ) def update(self, load_balancer, name=None, labels=None): # type:(LoadBalancer, Optional[str], Optional[Dict[str, str]]) -> BoundLoadBalancer """Updates a LoadBalancer. You can update a LoadBalancer’s name and a LoadBalancer’s labels. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundLoadBalancer ` """ data = {} if name is not None: data.update({"name": name}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url="/load_balancers/{load_balancer_id}".format( load_balancer_id=load_balancer.id ), method="PUT", json=data, ) return BoundLoadBalancer(self, response["load_balancer"]) def delete(self, load_balancer): # type: (LoadBalancer) -> BoundAction """Deletes a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :return: boolean """ self._client.request( url="/load_balancers/{load_balancer_id}".format( load_balancer_id=load_balancer.id ), method="DELETE", ) return True def get_actions_list( self, load_balancer, status=None, sort=None, page=None, per_page=None ): # type: (LoadBalancer, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] """Returns all action objects for a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ params = {} if status is not None: params["status"] = status if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/load_balancers/{load_balancer_id}/actions".format( load_balancer_id=load_balancer.id ), method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return add_meta_to_result(actions, response, "actions") def get_actions(self, load_balancer, status=None, sort=None): # type: (LoadBalancer, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return super(LoadBalancersClient, self).get_actions( load_balancer, status=status, sort=sort ) def add_service(self, load_balancer, service): # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction] """Adds a service to a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to add to the Load Balancer :return: :class:`BoundAction ` """ data = self.get_service_parameters(service) response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/add_service".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def get_service_parameters(self, service): data = {} if service.protocol is not None: data["protocol"] = service.protocol if service.listen_port is not None: data["listen_port"] = service.listen_port if service.destination_port is not None: data["destination_port"] = service.destination_port if service.proxyprotocol is not None: data["proxyprotocol"] = service.proxyprotocol if service.http is not None: data["http"] = {} if service.http.cookie_name is not None: data["http"]["cookie_name"] = service.http.cookie_name if service.http.cookie_lifetime is not None: data["http"]["cookie_lifetime"] = service.http.cookie_lifetime if service.http.redirect_http is not None: data["http"]["redirect_http"] = service.http.redirect_http if service.http.sticky_sessions is not None: data["http"]["sticky_sessions"] = service.http.sticky_sessions certificate_ids = [] for certificate in service.http.certificates: certificate_ids.append(certificate.id) data["http"]["certificates"] = certificate_ids if service.health_check is not None: data["health_check"] = { "protocol": service.health_check.protocol, "port": service.health_check.port, "interval": service.health_check.interval, "timeout": service.health_check.timeout, "retries": service.health_check.retries, } data["health_check"] = {} if service.health_check.protocol is not None: data["health_check"]["protocol"] = service.health_check.protocol if service.health_check.port is not None: data["health_check"]["port"] = service.health_check.port if service.health_check.interval is not None: data["health_check"]["interval"] = service.health_check.interval if service.health_check.timeout is not None: data["health_check"]["timeout"] = service.health_check.timeout if service.health_check.retries is not None: data["health_check"]["retries"] = service.health_check.retries if service.health_check.http is not None: data["health_check"]["http"] = {} if service.health_check.http.domain is not None: data["health_check"]["http"][ "domain" ] = service.health_check.http.domain if service.health_check.http.path is not None: data["health_check"]["http"][ "path" ] = service.health_check.http.path if service.health_check.http.response is not None: data["health_check"]["http"][ "response" ] = service.health_check.http.response if service.health_check.http.status_codes is not None: data["health_check"]["http"][ "status_codes" ] = service.health_check.http.status_codes if service.health_check.http.tls is not None: data["health_check"]["http"]["tls"] = service.health_check.http.tls return data def update_service(self, load_balancer, service): # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction] """Updates a service of an Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param service: :class:`LoadBalancerService ` The LoadBalancerService with updated values within for the Load Balancer :return: :class:`BoundAction ` """ data = self.get_service_parameters(service) response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/update_service".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def delete_service(self, load_balancer, service): # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction] """Deletes a service from a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to delete from the Load Balancer :return: :class:`BoundAction ` """ data = { "listen_port": service.listen_port, } response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/delete_service".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def add_target(self, load_balancer, target): # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerTarget) -> List[BoundAction] """Adds a target to a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param target: :class:`LoadBalancerTarget ` The LoadBalancerTarget you want to add to the Load Balancer :return: :class:`BoundAction ` """ data = {"type": target.type, "use_private_ip": target.use_private_ip} if target.type == "server": data["server"] = {"id": target.server.id} elif target.type == "label_selector": data["label_selector"] = {"selector": target.label_selector.selector} elif target.type == "ip": data["ip"] = {"ip": target.ip.ip} response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/add_target".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def remove_target(self, load_balancer, target): # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerTarget) -> List[BoundAction] """Removes a target from a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param target: :class:`LoadBalancerTarget ` The LoadBalancerTarget you want to remove from the Load Balancer :return: :class:`BoundAction ` """ data = { "type": target.type, } if target.type == "server": data["server"] = {"id": target.server.id} elif target.type == "label_selector": data["label_selector"] = {"selector": target.label_selector.selector} elif target.type == "ip": data["ip"] = {"ip": target.ip.ip} response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/remove_target".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def change_algorithm(self, load_balancer, algorithm): # type: (Union[LoadBalancer, BoundLoadBalancer], Optional[bool]) -> BoundAction """Changes the algorithm used by the Load Balancer :param load_balancer: :class:` ` or :class:`LoadBalancer ` :param algorithm: :class:`LoadBalancerAlgorithm ` The LoadBalancerSubnet you want to add to the Load Balancer :return: :class:`BoundAction ` """ data = {"type": algorithm.type} response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/change_algorithm".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def change_dns_ptr(self, load_balancer, ip, dns_ptr): # type: (Union[LoadBalancer, BoundLoadBalancer], str, str) -> BoundAction """Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer. :param ip: str The IP address for which to set the reverse DNS entry :param dns_ptr: str Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` :return: :class:`BoundAction ` """ response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/change_dns_ptr".format( load_balancer_id=load_balancer.id ), method="POST", json={"ip": ip, "dns_ptr": dns_ptr}, ) return BoundAction(self._client.actions, response["action"]) def change_protection(self, load_balancer, delete=None): # type: (Union[LoadBalancer, BoundLoadBalancer], Optional[bool]) -> BoundAction """Changes the protection configuration of a Load Balancer. :param load_balancer: :class:` ` or :class:`LoadBalancer ` :param delete: boolean If True, prevents the Load Balancer from being deleted :return: :class:`BoundAction ` """ data = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/change_protection".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def attach_to_network( self, load_balancer, # type: Union[LoadBalancer, BoundLoadBalancer] network, # type: Union[Network, BoundNetwork] ip=None, # type: Optional[str] ): """Attach a Load Balancer to a Network. :param load_balancer: :class:` ` or :class:`LoadBalancer ` :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this Load Balancer :return: :class:`BoundAction ` """ data = {"network": network.id} if ip is not None: data.update({"ip": ip}) response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/attach_to_network".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def detach_from_network(self, load_balancer, network): # type: (Union[LoadBalancer, BoundLoadBalancer], Union[Network,BoundNetwork]) -> BoundAction """Detaches a Load Balancer from a Network. :param load_balancer: :class:` ` or :class:`LoadBalancer ` :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`BoundAction ` """ data = { "network": network.id, } response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/detach_from_network".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def enable_public_interface(self, load_balancer): # type: (Union[LoadBalancer, BoundLoadBalancer]) -> BoundAction """Enables the public interface of a Load Balancer. :param load_balancer: :class:` ` or :class:`LoadBalancer ` :return: :class:`BoundAction ` """ response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/enable_public_interface".format( load_balancer_id=load_balancer.id ), method="POST", ) return BoundAction(self._client.actions, response["action"]) def disable_public_interface(self, load_balancer): # type: (Union[LoadBalancer, BoundLoadBalancer]) -> BoundAction """Disables the public interface of a Load Balancer. :param load_balancer: :class:` ` or :class:`LoadBalancer ` :return: :class:`BoundAction ` """ response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/disable_public_interface".format( load_balancer_id=load_balancer.id ), method="POST", ) return BoundAction(self._client.actions, response["action"]) def change_type(self, load_balancer, load_balancer_type): # type: ([LoadBalancer, BoundLoadBalancer], [LoadBalancerType, BoundLoadBalancerType]) ->BoundAction """Changes the type of a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param load_balancer_type: :class:`BoundLoadBalancerType ` or :class:`LoadBalancerType ` Load Balancer type the Load Balancer should migrate to :return: :class:`BoundAction ` """ data = { "load_balancer_type": load_balancer_type.id_or_name, } response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/change_type".format( load_balancer_id=load_balancer.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/load_balancers/domain.py0000644000175100001710000002520700000000000020453 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain class LoadBalancer(BaseDomain): """LoadBalancer Domain :param id: int ID of the Load Balancer :param name: str Name of the Load Balancer (must be unique per project) :param created: datetime Point in time when the Load Balancer was created :param protection: dict Protection configuration for the Load Balancer :param labels: dict User-defined labels (key-value pairs) :param location: Location Location of the Load Balancer :param public_net: :class:`PublicNetwork ` Public network information. :param private_net: List[:class:`PrivateNet ` :param ipv6: :class:`IPv6Network ` :param enabled: boolean """ __slots__ = ("ipv4", "ipv6", "enabled") def __init__( self, ipv4, # type: IPv4Address ipv6, # type: IPv6Network enabled, # type: bool ): self.ipv4 = ipv4 self.ipv6 = ipv6 self.enabled = enabled class IPv4Address(BaseDomain): """IPv4 Address Domain :param ip: str The IPv4 Address """ __slots__ = ( "ip", "dns_ptr", ) def __init__( self, ip, # type: str dns_ptr, # type: str ): self.ip = ip self.dns_ptr = dns_ptr class IPv6Network(BaseDomain): """IPv6 Network Domain :param ip: str The IPv6 Network as CIDR Notation """ __slots__ = ( "ip", "dns_ptr", ) def __init__( self, ip, # type: str dns_ptr, # type: str ): self.ip = ip self.dns_ptr = dns_ptr class PrivateNet(BaseDomain): """PrivateNet Domain :param network: :class:`BoundNetwork ` The Network the LoadBalancer is attached to :param ip: str The main IP Address of the LoadBalancer in the Network """ __slots__ = ( "network", "ip", ) def __init__( self, network, # type: BoundNetwork ip, # type: str ): self.network = network self.ip = ip class CreateLoadBalancerResponse(BaseDomain): """Create Load Balancer Response Domain :param load_balancer: :class:`BoundLoadBalancer ` The created Load Balancer :param action: :class:`BoundAction ` Shows the progress of the Load Balancer creation """ __slots__ = ( "load_balancer", "action", ) def __init__( self, load_balancer, # type: BoundLoadBalancer action, # type: BoundAction ): self.load_balancer = load_balancer self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/locations/0000755000175100001710000000000000000000000015666 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/locations/__init__.py0000644000175100001710000000003000000000000017770 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/locations/client.py0000644000175100001710000000504600000000000017523 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.locations.domain import Location class BoundLocation(BoundModelBase): model = Location class LocationsClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "locations" def get_by_id(self, id): # type: (int) -> locations.client.BoundLocation """Get a specific location by its ID. :param id: int :return: :class:`BoundLocation ` """ response = self._client.request( url="/locations/{location_id}".format(location_id=id), method="GET" ) return BoundLocation(self, response["location"]) def get_list(self, name=None, page=None, per_page=None): # type: (Optional[str], Optional[int], Optional[int]) -> PageResult[List[BoundLocation], Meta] """Get a list of locations :param name: str (optional) Can be used to filter locations by their name. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundLocation `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url="/locations", method="GET", params=params) locations = [ BoundLocation(self, location_data) for location_data in response["locations"] ] return self._add_meta_to_result(locations, response) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundLocation] """Get all locations :param name: str (optional) Can be used to filter locations by their name. :return: List[:class:`BoundLocation `] """ return super(LocationsClient, self).get_all(name=name) def get_by_name(self, name): # type: (str) -> BoundLocation """Get location by name :param name: str Used to get location by name. :return: :class:`BoundLocation ` """ return super(LocationsClient, self).get_by_name(name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/locations/domain.py0000644000175100001710000000255400000000000017515 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.domain import BaseDomain, DomainIdentityMixin class Location(BaseDomain, DomainIdentityMixin): """Location Domain :param id: int ID of location :param name: str Name of location :param description: str Description of location :param country: str ISO 3166-1 alpha-2 code of the country the location resides in :param city: str City the location is closest to :param latitude: float Latitude of the city closest to the location :param longitude: float Longitude of the city closest to the location :param network_zone: str Name of network zone this location resides in """ __slots__ = ( "id", "name", "description", "country", "city", "latitude", "longitude", "network_zone", ) def __init__( self, id=None, name=None, description=None, country=None, city=None, latitude=None, longitude=None, network_zone=None, ): self.id = id self.name = name self.description = description self.country = country self.city = city self.latitude = latitude self.longitude = longitude self.network_zone = network_zone ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/networks/0000755000175100001710000000000000000000000015547 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/networks/__init__.py0000644000175100001710000000003000000000000017651 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/networks/client.py0000644000175100001710000005123100000000000017401 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result from hcloud.actions.client import BoundAction from hcloud.networks.domain import Network, NetworkRoute, NetworkSubnet class BoundNetwork(BoundModelBase): model = Network def __init__(self, client, data, complete=True): subnets = data.get("subnets", []) if subnets is not None: subnets = [NetworkSubnet.from_dict(subnet) for subnet in subnets] data["subnets"] = subnets routes = data.get("routes", []) if routes is not None: routes = [NetworkRoute.from_dict(route) for route in routes] data["routes"] = routes from hcloud.servers.client import BoundServer servers = data.get("servers", []) if servers is not None: servers = [ BoundServer(client._client.servers, {"id": server}, complete=False) for server in servers ] data["servers"] = servers super(BoundNetwork, self).__init__(client, data, complete) def update(self, name=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]]) -> BoundNetwork """Updates a network. You can update a network’s name and a networks’s labels. :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` """ return self._client.update(self, name, labels) def delete(self): # type: () -> BoundAction """Deletes a network. :return: boolean """ return self._client.delete(self) def get_actions_list(self, status=None, sort=None, page=None, per_page=None): # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] """Returns all action objects for a network. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a network. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) def add_subnet(self, subnet): # type: (NetworkSubnet) -> List[BoundAction] """Adds a subnet entry to a network. :param subnet: :class:`NetworkSubnet ` The NetworkSubnet you want to add to the Network :return: :class:`BoundAction ` """ return self._client.add_subnet(self, subnet=subnet) def delete_subnet(self, subnet): # type: (NetworkSubnet) -> List[BoundAction] """Removes a subnet entry from a network :param subnet: :class:`NetworkSubnet ` The NetworkSubnet you want to remove from the Network :return: :class:`BoundAction ` """ return self._client.delete_subnet(self, subnet=subnet) def add_route(self, route): # type: (NetworkRoute) -> List[BoundAction] """Adds a route entry to a network. :param route: :class:`NetworkRoute ` The NetworkRoute you want to add to the Network :return: :class:`BoundAction ` """ return self._client.add_route(self, route=route) def delete_route(self, route): # type: (NetworkRoute) -> List[BoundAction] """Removes a route entry to a network. :param route: :class:`NetworkRoute ` The NetworkRoute you want to remove from the Network :return: :class:`BoundAction ` """ return self._client.delete_route(self, route=route) def change_ip_range(self, ip_range): # type: (str) -> List[BoundAction] """Changes the IP range of a network. :param ip_range: str The new prefix for the whole network. :return: :class:`BoundAction ` """ return self._client.change_ip_range(self, ip_range=ip_range) def change_protection(self, delete=None): # type: (Optional[bool]) -> BoundAction """Changes the protection configuration of a network. :param delete: boolean If True, prevents the network from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete=delete) class NetworksClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "networks" def get_by_id(self, id): # type: (int) -> BoundNetwork """Get a specific network :param id: int :return: :class:`BoundNetwork """ response = self._client.request( url="/networks/{network_id}".format(network_id=id), method="GET" ) return BoundNetwork(self, response["network"]) def get_list( self, name=None, # type: Optional[str] label_selector=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundNetwork], Meta] """Get a list of networks from this account :param name: str (optional) Can be used to filter networks by their name. :param label_selector: str (optional) Can be used to filter networks by labels. The response will only contain networks matching the label selector. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundNetwork `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if label_selector is not None: params["label_selector"] = label_selector if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url="/networks", method="GET", params=params) ass_networks = [ BoundNetwork(self, network_data) for network_data in response["networks"] ] return self._add_meta_to_result(ass_networks, response) def get_all(self, name=None, label_selector=None): # type: (Optional[str], Optional[str]) -> List[BoundNetwork] """Get all networks from this account :param name: str (optional) Can be used to filter networks by their name. :param label_selector: str (optional) Can be used to filter networks by labels. The response will only contain networks matching the label selector. :return: List[:class:`BoundNetwork `] """ return super(NetworksClient, self).get_all( name=name, label_selector=label_selector ) def get_by_name(self, name): # type: (str) -> BoundNetwork """Get network by name :param name: str Used to get network by name. :return: :class:`BoundNetwork ` """ return super(NetworksClient, self).get_by_name(name) def create( self, name, # type: str ip_range, # type: str subnets=None, # type: Optional[List[NetworkSubnet]] routes=None, # type: Optional[List[NetworkRoute]] labels=None, # type: Optional[Dict[str, str]] ): """Creates a network with range ip_range. :param name: str Name of the network :param ip_range: str IP range of the whole network which must span all included subnets and route destinations :param subnets: List[:class:`NetworkSubnet `] Array of subnets allocated :param routes: List[:class:`NetworkRoute `] Array of routes set in this network :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` """ data = {"name": name, "ip_range": ip_range} if subnets is not None: data["subnets"] = [ { "type": subnet.type, "ip_range": subnet.ip_range, "network_zone": subnet.network_zone, } for subnet in subnets ] if routes is not None: data["routes"] = [ {"destination": route.destination, "gateway": route.gateway} for route in routes ] if labels is not None: data["labels"] = labels response = self._client.request(url="/networks", method="POST", json=data) return BoundNetwork(self, response["network"]) def update(self, network, name=None, labels=None): # type:(Network, Optional[str], Optional[Dict[str, str]]) -> BoundNetwork """Updates a network. You can update a network’s name and a network’s labels. :param network: :class:`BoundNetwork ` or :class:`Network ` :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` """ data = {} if name is not None: data.update({"name": name}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url="/networks/{network_id}".format(network_id=network.id), method="PUT", json=data, ) return BoundNetwork(self, response["network"]) def delete(self, network): # type: (Network) -> BoundAction """Deletes a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :return: boolean """ self._client.request( url="/networks/{network_id}".format(network_id=network.id), method="DELETE" ) return True def get_actions_list( self, network, status=None, sort=None, page=None, per_page=None ): # type: (Network, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] """Returns all action objects for a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ params = {} if status is not None: params["status"] = status if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/networks/{network_id}/actions".format(network_id=network.id), method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return add_meta_to_result(actions, response, "actions") def get_actions(self, network, status=None, sort=None): # type: (Network, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return super(NetworksClient, self).get_actions( network, status=status, sort=sort ) def add_subnet(self, network, subnet): # type: (Union[Network, BoundNetwork], NetworkSubnet) -> List[BoundAction] """Adds a subnet entry to a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param subnet: :class:`NetworkSubnet ` The NetworkSubnet you want to add to the Network :return: :class:`BoundAction ` """ data = { "type": subnet.type, "network_zone": subnet.network_zone, } if subnet.ip_range is not None: data["ip_range"] = subnet.ip_range if subnet.vswitch_id is not None: data["vswitch_id"] = subnet.vswitch_id response = self._client.request( url="/networks/{network_id}/actions/add_subnet".format( network_id=network.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def delete_subnet(self, network, subnet): # type: (Union[Network, BoundNetwork], NetworkSubnet) -> List[BoundAction] """Removes a subnet entry from a network :param network: :class:`BoundNetwork ` or :class:`Network ` :param subnet: :class:`NetworkSubnet ` The NetworkSubnet you want to remove from the Network :return: :class:`BoundAction ` """ data = { "ip_range": subnet.ip_range, } response = self._client.request( url="/networks/{network_id}/actions/delete_subnet".format( network_id=network.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def add_route(self, network, route): # type: (Union[Network, BoundNetwork], NetworkRoute) -> List[BoundAction] """Adds a route entry to a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param route: :class:`NetworkRoute ` The NetworkRoute you want to add to the Network :return: :class:`BoundAction ` """ data = { "destination": route.destination, "gateway": route.gateway, } response = self._client.request( url="/networks/{network_id}/actions/add_route".format( network_id=network.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def delete_route(self, network, route): # type: (Union[Network, BoundNetwork], NetworkRoute) -> List[BoundAction] """Removes a route entry to a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param route: :class:`NetworkRoute ` The NetworkRoute you want to remove from the Network :return: :class:`BoundAction ` """ data = { "destination": route.destination, "gateway": route.gateway, } response = self._client.request( url="/networks/{network_id}/actions/delete_route".format( network_id=network.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def change_ip_range(self, network, ip_range): # type: (Union[Network, BoundNetwork], str) -> List[BoundAction] """Changes the IP range of a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip_range: str The new prefix for the whole network. :return: :class:`BoundAction ` """ data = { "ip_range": ip_range, } response = self._client.request( url="/networks/{network_id}/actions/change_ip_range".format( network_id=network.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def change_protection(self, network, delete=None): # type: (Union[Network, BoundNetwork], Optional[bool]) -> BoundAction """Changes the protection configuration of a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param delete: boolean If True, prevents the network from being deleted :return: :class:`BoundAction ` """ data = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url="/networks/{network_id}/actions/change_protection".format( network_id=network.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/networks/domain.py0000644000175100001710000000661700000000000017402 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain class Network(BaseDomain): """Network Domain :param id: int ID of the network :param name: str Name of the network :param ip_range: str IPv4 prefix of the whole network :param subnets: List[:class:`NetworkSubnet `] Subnets allocated in this network :param routes: List[:class:`NetworkRoute `] Routes set in this network :param servers: List[:class:`BoundServer `] Servers attached to this network :param protection: dict Protection configuration for the network :param labels: dict User-defined labels (key-value pairs) """ __slots__ = ( "id", "name", "ip_range", "subnets", "routes", "servers", "protection", "labels", "created", ) def __init__( self, id, name=None, created=None, ip_range=None, subnets=None, routes=None, servers=None, protection=None, labels=None, ): self.id = id self.name = name self.created = isoparse(created) if created else None self.ip_range = ip_range self.subnets = subnets self.routes = routes self.servers = servers self.protection = protection self.labels = labels class NetworkSubnet(BaseDomain): """Network Subnet Domain :param type: str Type of sub network. :param ip_range: str Range to allocate IPs from. :param network_zone: str Name of network zone. :param gateway: str Gateway for the route. :param vswitch_id: int ID of the vSwitch. """ TYPE_SERVER = "server" """Subnet Type server, deprecated, use TYPE_CLOUD instead""" TYPE_CLOUD = "cloud" """Subnet Type cloud""" TYPE_VSWITCH = "vswitch" """Subnet Type vSwitch""" __slots__ = ("type", "ip_range", "network_zone", "gateway", "vswitch_id") def __init__( self, ip_range, type=None, network_zone=None, gateway=None, vswitch_id=None ): self.type = type self.ip_range = ip_range self.network_zone = network_zone self.gateway = gateway self.vswitch_id = vswitch_id class NetworkRoute(BaseDomain): """Network Route Domain :param destination: str Destination network or host of this route. :param gateway: str Gateway for the route. """ __slots__ = ("destination", "gateway") def __init__(self, destination, gateway): self.destination = destination self.gateway = gateway class CreateNetworkResponse(BaseDomain): """Create Network Response Domain :param network: :class:`BoundNetwork ` The network which was created :param action: :class:`BoundAction ` The Action which shows the progress of the network Creation """ __slots__ = ("network", "action") def __init__( self, network, # type: BoundNetwork action, # type: BoundAction ): self.network = network self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/placement_groups/0000755000175100001710000000000000000000000017242 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/placement_groups/__init__.py0000644000175100001710000000003000000000000021344 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/placement_groups/client.py0000644000175100001710000001723000000000000021075 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.placement_groups.domain import PlacementGroup, CreatePlacementGroupResponse class BoundPlacementGroup(BoundModelBase): model = PlacementGroup def update(self, labels=None, name=None): # type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundPlacementGroup """Updates the name or labels of a Placement Group :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str, (optional) New Name to set :return: :class:`BoundPlacementGroup ` """ return self._client.update(self, labels, name) def delete(self): # type: () -> bool """Deletes a Placement Group :return: boolean """ return self._client.delete(self) class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "placement_groups" def get_by_id(self, id): # type: (int) -> BoundPlacementGroup """Returns a specific Placement Group object :param id: int :return: :class:`BoundPlacementGroup ` """ response = self._client.request( url="/placement_groups/{placement_group_id}".format(placement_group_id=id), method="GET", ) return BoundPlacementGroup(self, response["placement_group"]) def get_list( self, label_selector=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] name=None, # type: Optional[str] sort=None, # type: Optional[List[str]] type=None, # type: Optional[str] ): # type: (...) -> PageResults[List[BoundPlacementGroup]] """Get a list of Placement Groups :param label_selector: str (optional) Can be used to filter Placement Groups by labels. The response will only contain Placement Groups matching the label selector values. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :param name: str (optional) Can be used to filter Placement Groups by their name. :param sort: List[str] (optional) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: (List[:class:`BoundPlacementGroup `], :class:`Meta `) """ params = {} if label_selector is not None: params["label_selector"] = label_selector if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page if name is not None: params["name"] = name if sort is not None: params["sort"] = sort if type is not None: params["type"] = type response = self._client.request( url="/placement_groups", method="GET", params=params ) placement_groups = [ BoundPlacementGroup(self, placement_group_data) for placement_group_data in response["placement_groups"] ] return self._add_meta_to_result(placement_groups, response) def get_all(self, label_selector=None, name=None, sort=None): # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundPlacementGroup] """Get all Placement Groups :param label_selector: str (optional) Can be used to filter Placement Groups by labels. The response will only contain Placement Groups matching the label selector values. :param name: str (optional) Can be used to filter Placement Groups by their name. :param sort: List[str] (optional) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: List[:class:`BoundPlacementGroup `] """ return super(PlacementGroupsClient, self).get_all( label_selector=label_selector, name=name, sort=sort ) def get_by_name(self, name): # type: (str) -> BoundPlacementGroup """Get Placement Group by name :param name: str Used to get Placement Group by name :return: class:`BoundPlacementGroup ` """ return super(PlacementGroupsClient, self).get_by_name(name) def create( self, name, # type: str type, # type: str labels=None, # type: Optional[Dict[str, str]] ): # type: (...) -> CreatePlacementGroupResponse """Creates a new Placement Group. :param name: str Placement Group Name :param type: str Type of the Placement Group :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`CreatePlacementGroupResponse ` """ data = {"name": name, "type": type} if labels is not None: data["labels"] = labels response = self._client.request( url="/placement_groups", json=data, method="POST" ) action = None if response.get("action") is not None: action = BoundAction(self._client.action, response["action"]) result = CreatePlacementGroupResponse( placement_group=BoundPlacementGroup(self, response["placement_group"]), action=action, ) return result def update(self, placement_group, labels=None, name=None): # type: (PlacementGroup, Optional[Dict[str, str]], Optional[str]) -> BoundPlacementGroup """Updates the description or labels of a Placement Group. :param placement_group: :class:`BoundPlacementGroup ` or :class:`PlacementGroup ` :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str (optional) New name to set :return: :class:`BoundPlacementGroup ` """ data = {} if labels is not None: data["labels"] = labels if name is not None: data["name"] = name response = self._client.request( url="/placement_groups/{placement_group_id}".format( placement_group_id=placement_group.id ), method="PUT", json=data, ) return BoundPlacementGroup(self, response["placement_group"]) def delete(self, placement_group): # type: (PlacementGroup) -> bool """Deletes a Placement Group. :param placement_group: :class:`BoundPlacementGroup ` or :class:`PlacementGroup ` :return: boolean """ self._client.request( url="/placement_groups/{placement_group_id}".format( placement_group_id=placement_group.id ), method="DELETE", ) return True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/placement_groups/domain.py0000644000175100001710000000336700000000000021074 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain class PlacementGroup(BaseDomain): """Placement Group Domain :param id: int ID of the Placement Group :param name: str Name of the Placement Group :param labels: dict User-defined labels (key-value pairs) :param servers: List[ int ] List of server IDs assigned to the Placement Group :param type: str Type of the Placement Group :param created: datetime Point in time when the image was created """ __slots__ = ("id", "name", "labels", "servers", "type", "created") """Placement Group type spread spreads all servers in the group on different vhosts """ TYPE_SPREAD = "spread" def __init__( self, id=None, name=None, labels=None, servers=None, type=None, created=None ): self.id = id self.name = name self.labels = labels self.servers = servers self.type = type self.created = isoparse(created) if created else None class CreatePlacementGroupResponse(BaseDomain): """Create Placement Group Response Domain :param placement_group: :class:`BoundPlacementGroup ` The Placement Group which was created :param action: :class:`BoundAction ` The Action which shows the progress of the Placement Group Creation """ __slots__ = ("placement_group", "action") def __init__( self, placement_group, # type: BoundPlacementGroup action, # type: BoundAction ): self.placement_group = placement_group self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/server_types/0000755000175100001710000000000000000000000016425 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/server_types/__init__.py0000644000175100001710000000003000000000000020527 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/server_types/client.py0000644000175100001710000000521400000000000020257 0ustar00runnerdockerfrom hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.server_types.domain import ServerType class BoundServerType(BoundModelBase): model = ServerType class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "server_types" def get_by_id(self, id): # type: (int) -> server_types.client.BoundServerType """Returns a specific Server Type. :param id: int :return: :class:`BoundServerType ` """ response = self._client.request( url="/server_types/{server_type_id}".format(server_type_id=id), method="GET" ) return BoundServerType(self, response["server_type"]) def get_list(self, name=None, page=None, per_page=None): # type: (Optional[str], Optional[int], Optional[int]) -> PageResults[List[BoundServerType], Meta] """Get a list of Server types :param name: str (optional) Can be used to filter server type by their name. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundServerType `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/server_types", method="GET", params=params ) server_types = [ BoundServerType(self, server_type_data) for server_type_data in response["server_types"] ] return self._add_meta_to_result(server_types, response) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundServerType] """Get all Server types :param name: str (optional) Can be used to filter server type by their name. :return: List[:class:`BoundServerType `] """ return super(ServerTypesClient, self).get_all(name=name) def get_by_name(self, name): # type: (str) -> BoundServerType """Get Server type by name :param name: str Used to get Server type by name. :return: :class:`BoundServerType ` """ return super(ServerTypesClient, self).get_by_name(name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/server_types/domain.py0000644000175100001710000000333700000000000020254 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.domain import BaseDomain, DomainIdentityMixin class ServerType(BaseDomain, DomainIdentityMixin): """ServerType Domain :param id: int ID of the server type :param name: str Unique identifier of the server type :param description: str Description of the server type :param cores: int Number of cpu cores a server of this type will have :param memory: int Memory a server of this type will have in GB :param disk: int Disk size a server of this type will have in GB :param prices: Dict Prices in different locations :param storage_type: str Type of server boot drive. Local has higher speed. Network has better availability. Choices: `local`, `network` :param cpu_type: string Type of cpu. Choices: `shared`, `dedicated` :param deprecated: bool True if server type is deprecated """ __slots__ = ( "id", "name", "description", "cores", "memory", "disk", "prices", "storage_type", "cpu_type", "deprecated", ) def __init__( self, id=None, name=None, description=None, cores=None, memory=None, disk=None, prices=None, storage_type=None, cpu_type=None, deprecated=None, ): self.id = id self.name = name self.description = description self.cores = cores self.memory = memory self.disk = disk self.prices = prices self.storage_type = storage_type self.cpu_type = cpu_type self.deprecated = deprecated ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/servers/0000755000175100001710000000000000000000000015364 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/servers/__init__.py0000644000175100001710000000003000000000000017466 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/servers/client.py0000644000175100001710000014125500000000000017224 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.actions.client import BoundAction from hcloud.core.domain import add_meta_to_result from hcloud.firewalls.client import BoundFirewall from hcloud.floating_ips.client import BoundFloatingIP from hcloud.isos.client import BoundIso from hcloud.servers.domain import ( Server, CreateServerResponse, ResetPasswordResponse, EnableRescueResponse, RequestConsoleResponse, PublicNetwork, IPv4Address, IPv6Network, PrivateNet, PublicNetworkFirewall, ) from hcloud.volumes.client import BoundVolume from hcloud.images.domain import CreateImageResponse from hcloud.images.client import BoundImage from hcloud.server_types.client import BoundServerType from hcloud.datacenters.client import BoundDatacenter from hcloud.networks.client import BoundNetwork # noqa from hcloud.networks.domain import Network # noqa from hcloud.placement_groups.client import BoundPlacementGroup class BoundServer(BoundModelBase): model = Server def __init__(self, client, data, complete=True): datacenter = data.get("datacenter") if datacenter is not None: data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) volumes = data.get("volumes", []) if volumes: volumes = [ BoundVolume(client._client.volumes, {"id": volume}, complete=False) for volume in volumes ] data["volumes"] = volumes image = data.get("image", None) if image is not None: data["image"] = BoundImage(client._client.images, image) iso = data.get("iso", None) if iso is not None: data["iso"] = BoundIso(client._client.isos, iso) server_type = data.get("server_type") if server_type is not None: data["server_type"] = BoundServerType( client._client.server_types, server_type ) public_net = data.get("public_net") if public_net: ipv4_address = IPv4Address.from_dict(public_net["ipv4"]) ipv6_network = IPv6Network.from_dict(public_net["ipv6"]) floating_ips = [ BoundFloatingIP( client._client.floating_ips, {"id": floating_ip}, complete=False ) for floating_ip in public_net["floating_ips"] ] firewalls = [ PublicNetworkFirewall( BoundFirewall( client._client.firewalls, {"id": firewall["id"]}, complete=False ), status=firewall["status"], ) for firewall in public_net.get("firewalls", []) ] data["public_net"] = PublicNetwork( ipv4=ipv4_address, ipv6=ipv6_network, floating_ips=floating_ips, firewalls=firewalls, ) private_nets = data.get("private_net") if private_nets: private_nets = [ PrivateNet( network=BoundNetwork( client._client.networks, {"id": private_net["network"]}, complete=False, ), ip=private_net["ip"], alias_ips=private_net["alias_ips"], mac_address=private_net["mac_address"], ) for private_net in private_nets ] data["private_net"] = private_nets placement_group = data.get("placement_group") if placement_group: placement_group = BoundPlacementGroup( client._client.placement_groups, placement_group ) data["placement_group"] = placement_group super(BoundServer, self).__init__(client, data, complete) def get_actions_list(self, status=None, sort=None, page=None, per_page=None): # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] """Returns all action objects for a server. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a server. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) def update(self, name=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]]) -> BoundServer """Updates a server. You can update a server’s name and a server’s labels. :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundServer ` """ return self._client.update(self, name, labels) def delete(self): # type: () -> BoundAction """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. :return: :class:`BoundAction ` """ return self._client.delete(self) def power_off(self): # type: () -> BoundAction """Cuts power to the server. This forcefully stops it without giving the server operating system time to gracefully stop :return: :class:`BoundAction ` """ return self._client.power_off(self) def power_on(self): # type: () -> BoundAction """Starts a server by turning its power on. :return: :class:`BoundAction ` """ return self._client.power_on(self) def reboot(self): # type: () -> BoundAction """Reboots a server gracefully by sending an ACPI request. :return: :class:`BoundAction ` """ return self._client.reboot(self) def reset(self): # type: () -> BoundAction """Cuts power to a server and starts it again. :return: :class:`BoundAction ` """ return self._client.reset(self) def shutdown(self): # type: () -> BoundAction """Shuts down a server gracefully by sending an ACPI shutdown request. :return: :class:`BoundAction ` """ return self._client.shutdown(self) def reset_password(self): # type: () -> ResetPasswordResponse """Resets the root password. Only works for Linux systems that are running the qemu guest agent. :return: :class:`ResetPasswordResponse ` """ return self._client.reset_password(self) def enable_rescue(self, type=None, ssh_keys=None): # type: (str, Optional[List[str]]) -> EnableRescueResponse """Enable the Hetzner Rescue System for this server. :param type: str Type of rescue system to boot (default: linux64) Choices: linux64, linux32, freebsd64 :param ssh_keys: List[str] Array of SSH key IDs which should be injected into the rescue system. Only available for types: linux64 and linux32. :return: :class:`EnableRescueResponse ` """ return self._client.enable_rescue(self, type=type, ssh_keys=ssh_keys) def disable_rescue(self): # type: () -> BoundAction """Disables the Hetzner Rescue System for a server. :return: :class:`BoundAction ` """ return self._client.disable_rescue(self) def create_image(self, description=None, type=None, labels=None): # type: (str, str, Optional[Dict[str, str]]) -> CreateImageResponse """Creates an image (snapshot) from a server by copying the contents of its disks. :param description: str (optional) Description of the image. If you do not set this we auto-generate one for you. :param type: str (optional) Type of image to create (default: snapshot) Choices: snapshot, backup :param labels: Dict[str, str] User-defined labels (key-value pairs) :return: :class:`CreateImageResponse ` """ return self._client.create_image(self, description, type, labels) def rebuild(self, image): # type: (Image) -> BoundAction """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. :param image: :class:`BoundImage ` or :class:`Image ` :return: :class:`BoundAction ` """ return self._client.rebuild(self, image) def change_type(self, server_type, upgrade_disk): # type: (BoundServerType, bool) -> BoundAction """Changes the type (Cores, RAM and disk sizes) of a server. :param server_type: :class:`BoundServerType ` or :class:`ServerType ` Server type the server should migrate to :param upgrade_disk: boolean If false, do not upgrade the disk. This allows downgrading the server type later. :return: :class:`BoundAction ` """ return self._client.change_type(self, server_type, upgrade_disk) def enable_backup(self): # type: () -> BoundAction """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. :return: :class:`BoundAction ` """ return self._client.enable_backup(self) def disable_backup(self): # type: () -> BoundAction """Disables the automatic backup option and deletes all existing Backups for a Server. :return: :class:`BoundAction ` """ return self._client.disable_backup(self) def attach_iso(self, iso): # type: (Iso) -> BoundAction """Attaches an ISO to a server. :param iso: :class:`BoundIso ` or :class:`Server ` :return: :class:`BoundAction ` """ return self._client.attach_iso(self, iso) def detach_iso(self): # type: () -> BoundAction """Detaches an ISO from a server. :return: :class:`BoundAction ` """ return self._client.detach_iso(self) def change_dns_ptr(self, ip, dns_ptr): # type: (str, Optional[str]) -> BoundAction """Changes the hostname that will appear when getting the hostname belonging to the primary IPs (ipv4 and ipv6) of this server. :param ip: str The IP address for which to set the reverse DNS entry :param dns_ptr: Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` :return: :class:`BoundAction ` """ return self._client.change_dns_ptr(self, ip, dns_ptr) def change_protection(self, delete=None, rebuild=None): # type: (Optional[bool], Optional[bool]) -> BoundAction """Changes the protection configuration of the server. :param server: :class:`BoundServer ` or :class:`Server ` :param delete: boolean If true, prevents the server from being deleted (currently delete and rebuild attribute needs to have the same value) :param rebuild: boolean If true, prevents the server from being rebuilt (currently delete and rebuild attribute needs to have the same value) :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete, rebuild) def request_console(self): # type: () -> RequestConsoleResponse """Requests credentials for remote access via vnc over websocket to keyboard, monitor, and mouse for a server. :return: :class:`RequestConsoleResponse ` """ return self._client.request_console(self) def attach_to_network(self, network, ip=None, alias_ips=None): # type: (Union[Network,BoundNetwork],Optional[str], Optional[List[str]]) -> BoundAction """Attaches a server to a network :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this server :param alias_ips: List[str] New alias IPs to set for this server. :return: :class:`BoundAction ` """ return self._client.attach_to_network(self, network, ip, alias_ips) def detach_from_network(self, network): # type: ( Union[Network,BoundNetwork]) -> BoundAction """Detaches a server from a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`BoundAction ` """ return self._client.detach_from_network(self, network) def change_alias_ips(self, network, alias_ips): # type: (Union[Network,BoundNetwork], List[str]) -> BoundAction """Changes the alias IPs of an already attached network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param alias_ips: List[str] New alias IPs to set for this server. :return: :class:`BoundAction ` """ return self._client.change_alias_ips(self, network, alias_ips) def add_to_placement_group(self, placement_group): # type: (Union[PlacementGroup,BoundPlacementGroup]) -> BoundAction """Adds a server to a placement group. :param placement_group: :class:`BoundPlacementGroup ` or :class:`Network ` :return: :class:`BoundAction ` """ return self._client.add_to_placement_group(self, placement_group) def remove_from_placement_group(self): # type: () -> BoundAction """Removes a server from a placement group. :return: :class:`BoundAction ` """ return self._client.remove_from_placement_group(self) class ServersClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "servers" def get_by_id(self, id): # type: (int) -> BoundServer """Get a specific server :param id: int :return: :class:`BoundServer ` """ response = self._client.request( url="/servers/{server_id}".format(server_id=id), method="GET" ) return BoundServer(self, response["server"]) def get_list( self, name=None, # type: Optional[str] label_selector=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] status=None, # type: Optional[List[str]] ): # type: (...) -> PageResults[List[BoundServer], Meta] """Get a list of servers from this account :param name: str (optional) Can be used to filter servers by their name. :param label_selector: str (optional) Can be used to filter servers by labels. The response will only contain servers matching the label selector. :param status: List[str] (optional) Can be used to filter servers by their status. The response will only contain servers matching the status. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundServer `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if label_selector is not None: params["label_selector"] = label_selector if status is not None: params["status"] = status if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url="/servers", method="GET", params=params) ass_servers = [ BoundServer(self, server_data) for server_data in response["servers"] ] return self._add_meta_to_result(ass_servers, response) def get_all(self, name=None, label_selector=None, status=None): # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundServer] """Get all servers from this account :param name: str (optional) Can be used to filter servers by their name. :param label_selector: str (optional) Can be used to filter servers by labels. The response will only contain servers matching the label selector. :param status: List[str] (optional) Can be used to filter servers by their status. The response will only contain servers matching the status. :return: List[:class:`BoundServer `] """ return super(ServersClient, self).get_all( name=name, label_selector=label_selector, status=status ) def get_by_name(self, name): # type: (str) -> BoundServer """Get server by name :param name: str Used to get server by name. :return: :class:`BoundServer ` """ return super(ServersClient, self).get_by_name(name) def create( self, name, # type: str server_type, # type: ServerType image, # type: Image ssh_keys=None, # type: Optional[List[SSHKey]] volumes=None, # type: Optional[List[Volume]] firewalls=None, # type: Optional[List[Firewall]] networks=None, # type: Optional[List[Network]] user_data=None, # type: Optional[str] labels=None, # type: Optional[Dict[str, str]] location=None, # type: Optional[Location] datacenter=None, # type: Optional[Datacenter] start_after_create=True, # type: Optional[bool] automount=None, # type: Optional[bool] placement_group=None, # type: Optional[PlacementGroup] ): # type: (...) -> CreateServerResponse """Creates a new server. Returns preliminary information about the server as well as an action that covers progress of creation. :param name: str Name of the server to create (must be unique per project and a valid hostname as per RFC 1123) :param server_type: :class:`BoundServerType ` or :class:`ServerType ` Server type this server should be created with :param image: :class:`BoundImage ` or :class:`Image ` Image the server is created from :param ssh_keys: List[:class:`BoundSSHKey ` or :class:`SSHKey `] (optional) SSH keys which should be injected into the server at creation time :param volumes: List[:class:`BoundVolume ` or :class:`Volume `] (optional) Volumes which should be attached to the server at the creation time. Volumes must be in the same location. :param networks: List[:class:`BoundNetwork ` or :class:`Network `] (optional) Networks which should be attached to the server at the creation time. :param user_data: str (optional) Cloud-Init user data to use during server creation. This field is limited to 32KiB. :param labels: Dict[str,str] (optional) User-defined labels (key-value pairs) :param location: :class:`BoundLocation ` or :class:`Location ` :param datacenter: :class:`BoundDatacenter ` or :class:`Datacenter ` :param start_after_create: boolean (optional) Start Server right after creation. Defaults to True. :param automount: boolean (optional) Auto mount volumes after attach. :param placement_group: :class:`BoundPlacementGroup ` or :class:`Location ` Placement Group where server should be added during creation :return: :class:`CreateServerResponse ` """ data = { "name": name, "server_type": server_type.id_or_name, "start_after_create": start_after_create, "image": image.id_or_name, } if location is not None: data["location"] = location.id_or_name if datacenter is not None: data["datacenter"] = datacenter.id_or_name if ssh_keys is not None: data["ssh_keys"] = [ssh_key.id_or_name for ssh_key in ssh_keys] if volumes is not None: data["volumes"] = [volume.id for volume in volumes] if networks is not None: data["networks"] = [network.id for network in networks] if firewalls is not None: data["firewalls"] = [{"firewall": firewall.id} for firewall in firewalls] if user_data is not None: data["user_data"] = user_data if labels is not None: data["labels"] = labels if automount is not None: data["automount"] = automount if placement_group is not None: data["placement_group"] = placement_group.id response = self._client.request(url="/servers", method="POST", json=data) result = CreateServerResponse( server=BoundServer(self, response["server"]), action=BoundAction(self._client.actions, response["action"]), next_actions=[ BoundAction(self._client.actions, action) for action in response["next_actions"] ], root_password=response["root_password"], ) return result def get_actions_list( self, server, status=None, sort=None, page=None, per_page=None ): # type: (Server, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] """Returns all action objects for a server. :param server: :class:`BoundServer ` or :class:`Server ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ params = {} if status is not None: params["status"] = status if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/servers/{server_id}/actions".format(server_id=server.id), method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return add_meta_to_result(actions, response, "actions") def get_actions(self, server, status=None, sort=None): # type: (Server, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a server. :param server: :class:`BoundServer ` or :class:`Server ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return super(ServersClient, self).get_actions(server, status=status, sort=sort) def update(self, server, name=None, labels=None): # type:(Server, Optional[str], Optional[Dict[str, str]]) -> BoundServer """Updates a server. You can update a server’s name and a server’s labels. :param server: :class:`BoundServer ` or :class:`Server ` :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundServer ` """ data = {} if name is not None: data.update({"name": name}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url="/servers/{server_id}".format(server_id=server.id), method="PUT", json=data, ) return BoundServer(self, response["server"]) def delete(self, server): # type: (Server) -> BoundAction """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}".format(server_id=server.id), method="DELETE" ) return BoundAction(self._client.actions, response["action"]) def power_off(self, server): # type: (Server) -> Action """Cuts power to the server. This forcefully stops it without giving the server operating system time to gracefully stop :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/poweroff".format(server_id=server.id), method="POST", ) return BoundAction(self._client.actions, response["action"]) def power_on(self, server): # type: (servers.domain.Server) -> actions.domain.Action """Starts a server by turning its power on. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/poweron".format(server_id=server.id), method="POST", ) return BoundAction(self._client.actions, response["action"]) def reboot(self, server): # type: (servers.domain.Server) -> actions.domain.Action """Reboots a server gracefully by sending an ACPI request. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/reboot".format(server_id=server.id), method="POST", ) return BoundAction(self._client.actions, response["action"]) def reset(self, server): # type: (servers.domain.Server) -> actions.domainAction """Cuts power to a server and starts it again. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/reset".format(server_id=server.id), method="POST", ) return BoundAction(self._client.actions, response["action"]) def shutdown(self, server): # type: (servers.domain.Server) -> actions.domainAction """Shuts down a server gracefully by sending an ACPI shutdown request. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/shutdown".format(server_id=server.id), method="POST", ) return BoundAction(self._client.actions, response["action"]) def reset_password(self, server): # type: (servers.domain.Server) -> ResetPasswordResponse """Resets the root password. Only works for Linux systems that are running the qemu guest agent. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`ResetPasswordResponse ` """ response = self._client.request( url="/servers/{server_id}/actions/reset_password".format( server_id=server.id ), method="POST", ) return ResetPasswordResponse( action=BoundAction(self._client.actions, response["action"]), root_password=response["root_password"], ) def change_type(self, server, server_type, upgrade_disk): # type: (servers.domain.Server, BoundServerType, bool) -> actions.domainAction """Changes the type (Cores, RAM and disk sizes) of a server. :param server: :class:`BoundServer ` or :class:`Server ` :param server_type: :class:`BoundServerType ` or :class:`ServerType ` Server type the server should migrate to :param upgrade_disk: boolean If false, do not upgrade the disk. This allows downgrading the server type later. :return: :class:`BoundAction ` """ data = {"server_type": server_type.id_or_name, "upgrade_disk": upgrade_disk} response = self._client.request( url="/servers/{server_id}/actions/change_type".format(server_id=server.id), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def enable_rescue(self, server, type=None, ssh_keys=None): # type: (servers.domain.Server, str, Optional[List[str]]) -> EnableRescueResponse """Enable the Hetzner Rescue System for this server. :param server: :class:`BoundServer ` or :class:`Server ` :param type: str Type of rescue system to boot (default: linux64) Choices: linux64, linux32, freebsd64 :param ssh_keys: List[str] Array of SSH key IDs which should be injected into the rescue system. Only available for types: linux64 and linux32. :return: :class:`EnableRescueResponse ` """ data = {"type": type} if ssh_keys is not None: data.update({"ssh_keys": ssh_keys}) response = self._client.request( url="/servers/{server_id}/actions/enable_rescue".format( server_id=server.id ), method="POST", json=data, ) return EnableRescueResponse( action=BoundAction(self._client.actions, response["action"]), root_password=response["root_password"], ) def disable_rescue(self, server): # type: (servers.domain.Server) -> actions.domainAction """Disables the Hetzner Rescue System for a server. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/disable_rescue".format( server_id=server.id ), method="POST", ) return BoundAction(self._client.actions, response["action"]) def create_image(self, server, description=None, type=None, labels=None): # type: (servers.domain.Server, str, str, Optional[Dict[str, str]]) -> CreateImageResponse """Creates an image (snapshot) from a server by copying the contents of its disks. :param server: :class:`BoundServer ` or :class:`Server ` :param description: str (optional) Description of the image. If you do not set this we auto-generate one for you. :param type: str (optional) Type of image to create (default: snapshot) Choices: snapshot, backup :param labels: Dict[str, str] User-defined labels (key-value pairs) :return: :class:`CreateImageResponse ` """ data = {} if description is not None: data.update({"description": description}) if type is not None: data.update({"type": type}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url="/servers/{server_id}/actions/create_image".format(server_id=server.id), method="POST", json=data, ) return CreateImageResponse( action=BoundAction(self._client.actions, response["action"]), image=BoundImage(self._client.images, response["image"]), ) def rebuild(self, server, image): # type: (servers.domain.Server, Image) -> actions.domainAction """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. :param server: :class:`BoundServer ` or :class:`Server ` :param image: :class:`BoundImage ` or :class:`Image ` :return: :class:`BoundAction ` """ data = {"image": image.id_or_name} response = self._client.request( url="/servers/{server_id}/actions/rebuild".format(server_id=server.id), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def enable_backup(self, server): # type: (servers.domain.Server) -> actions.domainAction """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/enable_backup".format( server_id=server.id ), method="POST", ) return BoundAction(self._client.actions, response["action"]) def disable_backup(self, server): # type: (servers.domain.Server) -> actions.domainAction """Disables the automatic backup option and deletes all existing Backups for a Server. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/disable_backup".format( server_id=server.id ), method="POST", ) return BoundAction(self._client.actions, response["action"]) def attach_iso(self, server, iso): # type: (servers.domain.Server, Iso) -> actions.domainAction """Attaches an ISO to a server. :param server: :class:`BoundServer ` or :class:`Server ` :param iso: :class:`BoundIso ` or :class:`Server ` :return: :class:`BoundAction ` """ data = {"iso": iso.id_or_name} response = self._client.request( url="/servers/{server_id}/actions/attach_iso".format(server_id=server.id), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def detach_iso(self, server): # type: (servers.domain.Server) -> actions.domainAction """Detaches an ISO from a server. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/detach_iso".format(server_id=server.id), method="POST", ) return BoundAction(self._client.actions, response["action"]) def change_dns_ptr(self, server, ip, dns_ptr): # type: (servers.domain.Server, str, str) -> actions.domainAction """Changes the hostname that will appear when getting the hostname belonging to the primary IPs (ipv4 and ipv6) of this server. :param server: :class:`BoundServer ` or :class:`Server ` :param ip: str The IP address for which to set the reverse DNS entry :param dns_ptr: Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` :return: :class:`BoundAction ` """ data = {"ip": ip, "dns_ptr": dns_ptr} response = self._client.request( url="/servers/{server_id}/actions/change_dns_ptr".format( server_id=server.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def change_protection(self, server, delete=None, rebuild=None): # type: (servers.domain.Server, Optional[bool], Optional[bool]) -> actions.domainAction """Changes the protection configuration of the server. :param server: :class:`BoundServer ` or :class:`Server ` :param delete: boolean If true, prevents the server from being deleted (currently delete and rebuild attribute needs to have the same value) :param rebuild: boolean If true, prevents the server from being rebuilt (currently delete and rebuild attribute needs to have the same value) :return: :class:`BoundAction ` """ data = {} if delete is not None: data.update({"delete": delete}) if rebuild is not None: data.update({"rebuild": rebuild}) response = self._client.request( url="/servers/{server_id}/actions/change_protection".format( server_id=server.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def request_console(self, server): # type: (servers.domain.Server) -> RequestConsoleResponse """Requests credentials for remote access via vnc over websocket to keyboard, monitor, and mouse for a server. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`RequestConsoleResponse ` """ response = self._client.request( url="/servers/{server_id}/actions/request_console".format( server_id=server.id ), method="POST", ) return RequestConsoleResponse( action=BoundAction(self._client.actions, response["action"]), wss_url=response["wss_url"], password=response["password"], ) def attach_to_network(self, server, network, ip=None, alias_ips=None): # type: (Union[Server,BoundServer], Union[Network,BoundNetwork],Optional[str], Optional[List[str]]) -> BoundAction """Attaches a server to a network :param server: :class:`BoundServer ` or :class:`Server ` :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this server :param alias_ips: List[str] New alias IPs to set for this server. :return: :class:`BoundAction ` """ data = { "network": network.id, } if ip is not None: data.update({"ip": ip}) if alias_ips is not None: data.update({"alias_ips": alias_ips}) response = self._client.request( url="/servers/{server_id}/actions/attach_to_network".format( server_id=server.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def detach_from_network(self, server, network): # type: (Union[Server,BoundServer], Union[Network,BoundNetwork]) -> BoundAction """Detaches a server from a network. :param server: :class:`BoundServer ` or :class:`Server ` :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`BoundAction ` """ data = { "network": network.id, } response = self._client.request( url="/servers/{server_id}/actions/detach_from_network".format( server_id=server.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def change_alias_ips(self, server, network, alias_ips): # type: (Union[Server,BoundServer], Union[Network,BoundNetwork], List[str]) -> BoundAction """Changes the alias IPs of an already attached network. :param server: :class:`BoundServer ` or :class:`Server ` :param network: :class:`BoundNetwork ` or :class:`Network ` :param alias_ips: List[str] New alias IPs to set for this server. :return: :class:`BoundAction ` """ data = {"network": network.id, "alias_ips": alias_ips} response = self._client.request( url="/servers/{server_id}/actions/change_alias_ips".format( server_id=server.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def add_to_placement_group(self, server, placement_group): # type: (Union[Server,BoundServer], Union[PlacementGroup,BoundPlacementGroup]) -> BoundAction """Adds a server to a placement group. :param server: :class:`BoundServer ` or :class:`Server ` :param placement_group: :class:`BoundPlacementGroup ` or :class:`Network ` :return: :class:`BoundAction ` """ data = { "placement_group": str(placement_group.id), } response = self._client.request( url="/servers/{server_id}/actions/add_to_placement_group".format( server_id=server.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def remove_from_placement_group(self, server): # type: (Union[Server,BoundServer]) -> BoundAction """Removes a server from a placement group. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url="/servers/{server_id}/actions/remove_from_placement_group".format( server_id=server.id ), method="POST", ) return BoundAction(self._client.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/servers/domain.py0000644000175100001710000002543700000000000017220 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain class Server(BaseDomain): """Server Domain :param id: int ID of the server :param name: str Name of the server (must be unique per project and a valid hostname as per RFC 1123) :param status: str Status of the server Choices: `running`, `initializing`, `starting`, `stopping`, `off`, `deleting`, `migrating`, `rebuilding`, `unknown` :param created: datetime Point in time when the server was created :param public_net: :class:`PublicNetwork ` Public network information. :param server_type: :class:`BoundServerType ` :param datacenter: :class:`BoundDatacenter ` :param image: :class:`BoundImage `, None :param iso: :class:`BoundIso `, None :param rescue_enabled: bool True if rescue mode is enabled: Server will then boot into rescue system on next reboot. :param locked: bool True if server has been locked and is not available to user. :param backup_window: str, None Time window (UTC) in which the backup will run, or None if the backups are not enabled :param outgoing_traffic: int, None Outbound Traffic for the current billing period in bytes :param ingoing_traffic: int, None Inbound Traffic for the current billing period in bytes :param included_traffic: int Free Traffic for the current billing period in bytes :param primary_disk_size: int Size of the primary Disk :param protection: dict Protection configuration for the server :param labels: dict User-defined labels (key-value pairs) :param volumes: List[:class:`BoundVolume `] Volumes assigned to this server. :param private_net: List[:class:`PrivateNet ` The created server :param action: :class:`BoundAction ` Shows the progress of the server creation :param next_actions: List[:class:`BoundAction `] Additional actions like a `start_server` action after the server creation :param root_password: str, None The root password of the server if no SSH-Key was given on server creation """ __slots__ = ("server", "action", "next_actions", "root_password") def __init__( self, server, # type: BoundServer action, # type: BoundAction next_actions, # type: List[Action] root_password, # type: str ): self.server = server self.action = action self.next_actions = next_actions self.root_password = root_password class ResetPasswordResponse(BaseDomain): """Reset Password Response Domain :param action: :class:`BoundAction ` Shows the progress of the server passwort reset action :param root_password: str The root password of the server """ __slots__ = ("action", "root_password") def __init__( self, action, # type: BoundAction root_password, # type: str ): self.action = action self.root_password = root_password class EnableRescueResponse(BaseDomain): """Enable Rescue Response Domain :param action: :class:`BoundAction ` Shows the progress of the server enable rescue action :param root_password: str The root password of the server in the rescue mode """ __slots__ = ("action", "root_password") def __init__( self, action, # type: BoundAction root_password, # type: str ): self.action = action self.root_password = root_password class RequestConsoleResponse(BaseDomain): """Request Console Response Domain :param action: :class:`BoundAction ` Shows the progress of the server request console action :param wss_url: str URL of websocket proxy to use. This includes a token which is valid for a limited time only. :param password: str VNC password to use for this connection. This password only works in combination with a wss_url with valid token. """ __slots__ = ("action", "wss_url", "password") def __init__( self, action, # type: BoundAction wss_url, # type: str password, # type: str ): self.action = action self.wss_url = wss_url self.password = password class PublicNetwork(BaseDomain): """Public Network Domain :param ipv4: :class:`IPv4Address ` :param ipv6: :class:`IPv6Network ` :param floating_ips: List[:class:`BoundFloatingIP `] :param firewalls: List[:class:`PublicNetworkFirewall `] """ __slots__ = ("ipv4", "ipv6", "floating_ips", "firewalls") def __init__( self, ipv4, # type: IPv4Address ipv6, # type: IPv6Network floating_ips, # type: List[BoundFloatingIP] firewalls=None, # type: List[PublicNetworkFirewall] ): self.ipv4 = ipv4 self.ipv6 = ipv6 self.floating_ips = floating_ips self.firewalls = firewalls class PublicNetworkFirewall(BaseDomain): """Public Network Domain :param firewall: :class:`BoundFirewall ` :param status: str """ __slots__ = ("firewall", "status") STATUS_APPLIED = "applied" """Public Network Firewall Status applied""" STATUS_PENDING = "pending" """Public Network Firewall Status pending""" def __init__( self, firewall, # type: BoundFirewall status, # type: str ): self.firewall = firewall self.status = status class IPv4Address(BaseDomain): """IPv4 Address Domain :param ip: str The IPv4 Address :param blocked: bool Determine if the IP is blocked :param dns_ptr: str DNS PTR for the ip """ __slots__ = ("ip", "blocked", "dns_ptr") def __init__( self, ip, # type: str blocked, # type: bool dns_ptr, # type: str ): self.ip = ip self.blocked = blocked self.dns_ptr = dns_ptr class IPv6Network(BaseDomain): """IPv6 Network Domain :param ip: str The IPv6 Network as CIDR Notation :param blocked: bool Determine if the Network is blocked :param dns_ptr: dict DNS PTR Records for the Network as Dict :param network: str The network without the network mask :param network_mask: str The network mask """ __slots__ = ("ip", "blocked", "dns_ptr", "network", "network_mask") def __init__( self, ip, # type: str blocked, # type: bool dns_ptr, # type: list ): self.ip = ip self.blocked = blocked self.dns_ptr = dns_ptr ip_parts = self.ip.split("/") # 2001:db8::/64 to 2001:db8:: and 64 self.network = ip_parts[0] self.network_mask = ip_parts[1] class PrivateNet(BaseDomain): """PrivateNet Domain :param network: :class:`BoundNetwork ` The network the server is attached to :param ip: str The main IP Address of the server in the Network :param alias_ips: List[str] The alias ips for a server :param mac_address: str The mac address of the interface on the server """ __slots__ = ("network", "ip", "alias_ips", "mac_address") def __init__( self, network, # type: BoundNetwork ip, # type: str alias_ips, # type: List[str] mac_address, # type: str ): self.network = network self.ip = ip self.alias_ips = alias_ips self.mac_address = mac_address ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/ssh_keys/0000755000175100001710000000000000000000000015523 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/ssh_keys/__init__.py0000644000175100001710000000003000000000000017625 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/ssh_keys/client.py0000644000175100001710000001633500000000000017363 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.ssh_keys.domain import SSHKey class BoundSSHKey(BoundModelBase): model = SSHKey def update(self, name=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey """Updates an SSH key. You can update an SSH key name and an SSH key labels. :param description: str (optional) New Description to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundSSHKey """ return self._client.update(self, name, labels) def delete(self): # type: () -> bool """Deletes an SSH key. It cannot be used anymore. :return: boolean """ return self._client.delete(self) class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "ssh_keys" def get_by_id(self, id): # type: (int) -> BoundSSHKey """Get a specific SSH Key by its ID :param id: int :return: :class:`BoundSSHKey ` """ response = self._client.request( url="/ssh_keys/{ssh_key_id}".format(ssh_key_id=id), method="GET" ) return BoundSSHKey(self, response["ssh_key"]) def get_list( self, name=None, # type: Optional[str] fingerprint=None, # type: Optional[str] label_selector=None, # type: Optional[str] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundSSHKey], Meta] """Get a list of SSH keys from the account :param name: str (optional) Can be used to filter SSH keys by their name. The response will only contain the SSH key matching the specified name. :param fingerprint: str (optional) Can be used to filter SSH keys by their fingerprint. The response will only contain the SSH key matching the specified fingerprint. :param label_selector: str (optional) Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundSSHKey `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if fingerprint is not None: params["fingerprint"] = fingerprint if label_selector is not None: params["label_selector"] = label_selector if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url="/ssh_keys", method="GET", params=params) ass_ssh_keys = [ BoundSSHKey(self, server_data) for server_data in response["ssh_keys"] ] return self._add_meta_to_result(ass_ssh_keys, response) def get_all(self, name=None, fingerprint=None, label_selector=None): # type: (Optional[str], Optional[str], Optional[str]) -> List[BoundSSHKey] """Get all SSH keys from the account :param name: str (optional) Can be used to filter SSH keys by their name. The response will only contain the SSH key matching the specified name. :param fingerprint: str (optional) Can be used to filter SSH keys by their fingerprint. The response will only contain the SSH key matching the specified fingerprint. :param label_selector: str (optional) Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. :return: List[:class:`BoundSSHKey `] """ return super(SSHKeysClient, self).get_all( name=name, fingerprint=fingerprint, label_selector=label_selector ) def get_by_name(self, name): # type: (str) -> SSHKeysClient """Get ssh key by name :param name: str Used to get ssh key by name. :return: :class:`BoundSSHKey ` """ return super(SSHKeysClient, self).get_by_name(name) def get_by_fingerprint(self, fingerprint): # type: (str) -> BoundSSHKey """Get ssh key by fingerprint :param fingerprint: str Used to get ssh key by fingerprint. :return: :class:`BoundSSHKey ` """ response = self.get_list(fingerprint=fingerprint) sshkeys = response.ssh_keys return sshkeys[0] if sshkeys else None def create(self, name, public_key, labels=None): # type: (str, str, Optional[Dict[str, str]]) -> BoundSSHKey """Creates a new SSH key with the given name and public_key. :param name: str :param public_key: str Public Key of the SSH Key you want create :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundSSHKey ` """ data = {"name": name, "public_key": public_key} if labels is not None: data["labels"] = labels response = self._client.request(url="/ssh_keys", method="POST", json=data) return BoundSSHKey(self, response["ssh_key"]) def update(self, ssh_key, name=None, labels=None): # type: (SSHKey, Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey """Updates an SSH key. You can update an SSH key name and an SSH key labels. :param ssh_key: :class:`BoundSSHKey ` or :class:`SSHKey ` :param name: str (optional) New Description to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundSSHKey ` """ data = {} if name is not None: data["name"] = name if labels is not None: data["labels"] = labels response = self._client.request( url="/ssh_keys/{ssh_key_id}".format(ssh_key_id=ssh_key.id), method="PUT", json=data, ) return BoundSSHKey(self, response["ssh_key"]) def delete(self, ssh_key): # type: (SSHKey) -> bool self._client.request( url="/ssh_keys/{ssh_key_id}".format(ssh_key_id=ssh_key.id), method="DELETE" ) """Deletes an SSH key. It cannot be used anymore. :param ssh_key: :class:`BoundSSHKey ` or :class:`SSHKey ` :return: True """ # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised return True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/ssh_keys/domain.py0000644000175100001710000000205500000000000017346 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin class SSHKey(BaseDomain, DomainIdentityMixin): """SSHKey Domain :param id: int ID of the SSH key :param name: str Name of the SSH key (must be unique per project) :param fingerprint: str Fingerprint of public key :param public_key: str Public Key :param labels: Dict User-defined labels (key-value pairs) :param created: datetime Point in time when the SSH Key was created """ __slots__ = ("id", "name", "fingerprint", "public_key", "labels", "created") def __init__( self, id=None, name=None, fingerprint=None, public_key=None, labels=None, created=None, ): self.id = id self.name = name self.fingerprint = fingerprint self.public_key = public_key self.labels = labels self.created = isoparse(created) if created else None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/hcloud/volumes/0000755000175100001710000000000000000000000015365 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/volumes/__init__.py0000644000175100001710000000003000000000000017467 0ustar00runnerdocker# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/volumes/client.py0000644000175100001710000004404600000000000017225 0ustar00runnerdocker# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.actions.client import BoundAction from hcloud.core.domain import add_meta_to_result from hcloud.volumes.domain import Volume, CreateVolumeResponse from hcloud.locations.client import BoundLocation class BoundVolume(BoundModelBase): model = Volume def __init__(self, client, data, complete=True): location = data.get("location") if location is not None: data["location"] = BoundLocation(client._client.locations, location) from hcloud.servers.client import BoundServer server = data.get("server") if server is not None: data["server"] = BoundServer( client._client.servers, {"id": server}, complete=False ) super(BoundVolume, self).__init__(client, data, complete) def get_actions_list(self, status=None, sort=None, page=None, per_page=None): # type: (Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] """Returns all action objects for a volume. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): # type: (Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a volume. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort:List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) def update(self, name=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]]) -> BoundAction """Updates the volume properties. :param name: str (optional) New volume name :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundAction ` """ return self._client.update(self, name, labels) def delete(self): # type: () -> BoundAction """Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled. :return: boolean """ return self._client.delete(self) def attach(self, server, automount=None): # type: (Union[Server, BoundServer]) -> BoundAction """Attaches a volume to a server. Works only if the server is in the same location as the volume. :param server: :class:`BoundServer ` or :class:`Server ` :param automount: boolean :return: :class:`BoundAction ` """ return self._client.attach(self, server, automount) def detach(self): # type: () -> BoundAction """Detaches a volume from the server it’s attached to. You may attach it to a server again at a later time. :return: :class:`BoundAction ` """ return self._client.detach(self) def resize(self, size): # type: (int) -> BoundAction """Changes the size of a volume. Note that downsizing a volume is not possible. :param size: int New volume size in GB (must be greater than current size) :return: :class:`BoundAction ` """ return self._client.resize(self, size) def change_protection(self, delete=None): # type: (Optional[bool]) -> BoundAction """Changes the protection configuration of a volume. :param delete: boolean If True, prevents the volume from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete) class VolumesClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "volumes" def get_by_id(self, id): # type: (int) -> volumes.client.BoundVolume """Get a specific volume by its id :param id: int :return: :class:`BoundVolume ` """ response = self._client.request( url="/volumes/{volume_id}".format(volume_id=id), method="GET" ) return BoundVolume(self, response["volume"]) def get_list( self, name=None, label_selector=None, page=None, per_page=None, status=None ): # type: (Optional[str], Optional[str], Optional[int], Optional[int], Optional[List[str]]) -> PageResults[List[BoundVolume], Meta] """Get a list of volumes from this account :param name: str (optional) Can be used to filter volumes by their name. :param label_selector: str (optional) Can be used to filter volumes by labels. The response will only contain volumes matching the label selector. :param status: List[str] (optional) Can be used to filter volumes by their status. The response will only contain volumes matching the status. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundVolume `], :class:`Meta `) """ params = {} if name is not None: params["name"] = name if label_selector is not None: params["label_selector"] = label_selector if status is not None: params["status"] = status if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url="/volumes", method="GET", params=params) volumes = [ BoundVolume(self, volume_data) for volume_data in response["volumes"] ] return self._add_meta_to_result(volumes, response) def get_all(self, label_selector=None, status=None): # type: (Optional[str], Optional[List[str]]) -> List[BoundVolume] """Get all volumes from this account :param label_selector: Can be used to filter volumes by labels. The response will only contain volumes matching the label selector. :param status: List[str] (optional) Can be used to filter volumes by their status. The response will only contain volumes matching the status. :return: List[:class:`BoundVolume `] """ return super(VolumesClient, self).get_all( label_selector=label_selector, status=status ) def get_by_name(self, name): # type: (str) -> BoundVolume """Get volume by name :param name: str Used to get volume by name. :return: :class:`BoundVolume ` """ return super(VolumesClient, self).get_by_name(name) def create( self, size, # type: int name, # type: str labels=None, # type: Optional[str] location=None, # type: Optional[Location] server=None, # type: Optional[Server], automount=None, # type: Optional[bool], format=None, # type: Optional[str], ): # type: (...) -> CreateVolumeResponse """Creates a new volume attached to a server. :param size: int Size of the volume in GB :param name: str Name of the volume :param labels: Dict[str,str] (optional) User-defined labels (key-value pairs) :param location: :class:`BoundLocation ` or :class:`Location ` :param server: :class:`BoundServer ` or :class:`Server ` :param automount: boolean (optional) Auto mount volumes after attach. :param format: str (optional) Format volume after creation. One of: xfs, ext4 :return: :class:`CreateVolumeResponse ` """ if size <= 0: raise ValueError("size must be greater than 0") if not (bool(location) ^ bool(server)): raise ValueError("only one of server or location must be provided") data = { "name": name, "size": size, } if labels is not None: data["labels"] = labels if location is not None: data["location"] = location.id_or_name if server is not None: data["server"] = server.id if automount is not None: data["automount"] = automount if format is not None: data["format"] = format response = self._client.request(url="/volumes", json=data, method="POST") result = CreateVolumeResponse( volume=BoundVolume(self, response["volume"]), action=BoundAction(self._client.actions, response["action"]), next_actions=[ BoundAction(self._client.actions, action) for action in response["next_actions"] ], ) return result def get_actions_list( self, volume, status=None, sort=None, page=None, per_page=None ): # type: (Volume, Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] """Returns all action objects for a volume. :param volume: :class:`BoundVolume ` or :class:`Volume ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ params = {} if status is not None: params["status"] = status if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( url="/volumes/{volume_id}/actions".format(volume_id=volume.id), method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return add_meta_to_result(actions, response, "actions") def get_actions(self, volume, status=None, sort=None): # type: (Union[Volume, BoundVolume], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a volume. :param volume: :class:`BoundVolume ` or :class:`Volume ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort:List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return super(VolumesClient, self).get_actions(volume, status=status, sort=sort) def update(self, volume, name=None, labels=None): # type:(Union[Volume, BoundVolume], Optional[str], Optional[Dict[str, str]]) -> BoundVolume """Updates the volume properties. :param volume: :class:`BoundVolume ` or :class:`Volume ` :param name: str (optional) New volume name :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundAction ` """ data = {} if name is not None: data.update({"name": name}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url="/volumes/{volume_id}".format(volume_id=volume.id), method="PUT", json=data, ) return BoundVolume(self, response["volume"]) def delete(self, volume): # type: (Union[Volume, BoundVolume]) -> BoundAction """Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled. :param volume: :class:`BoundVolume ` or :class:`Volume ` :return: boolean """ self._client.request( url="/volumes/{volume_id}".format(volume_id=volume.id), method="DELETE" ) return True def resize(self, volume, size): # type: (Union[Volume, BoundVolume], int) -> BoundAction """Changes the size of a volume. Note that downsizing a volume is not possible. :param volume: :class:`BoundVolume ` or :class:`Volume ` :param size: int New volume size in GB (must be greater than current size) :return: :class:`BoundAction ` """ data = self._client.request( url="/volumes/{volume_id}/actions/resize".format(volume_id=volume.id), json={"size": size}, method="POST", ) return BoundAction(self._client.actions, data["action"]) def attach(self, volume, server, automount=None): # type: (Union[Volume, BoundVolume], Union[Server, BoundServer], Optional[bool]) -> BoundAction """Attaches a volume to a server. Works only if the server is in the same location as the volume. :param volume: :class:`BoundVolume ` or :class:`Volume ` :param server: :class:`BoundServer ` or :class:`Server ` :param automount: boolean :return: :class:`BoundAction ` """ data = {"server": server.id} if automount is not None: data["automount"] = automount data = self._client.request( url="/volumes/{volume_id}/actions/attach".format(volume_id=volume.id), json=data, method="POST", ) return BoundAction(self._client.actions, data["action"]) def detach(self, volume): # type: (Union[Volume, BoundVolume]) -> BoundAction """Detaches a volume from the server it’s attached to. You may attach it to a server again at a later time. :param volume: :class:`BoundVolume ` or :class:`Volume ` :return: :class:`BoundAction ` """ data = self._client.request( url="/volumes/{volume_id}/actions/detach".format(volume_id=volume.id), method="POST", ) return BoundAction(self._client.actions, data["action"]) def change_protection(self, volume, delete=None): # type: (Union[Volume, BoundVolume], Optional[bool], Optional[bool]) -> BoundAction """Changes the protection configuration of a volume. :param volume: :class:`BoundVolume ` or :class:`Volume ` :param delete: boolean If True, prevents the volume from being deleted :return: :class:`BoundAction ` """ data = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url="/volumes/{volume_id}/actions/change_protection".format( volume_id=volume.id ), method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/hcloud/volumes/domain.py0000644000175100001710000000600200000000000017204 0ustar00runnerdocker# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin class Volume(BaseDomain, DomainIdentityMixin): """Volume Domain :param id: int ID of the Volume :param name: str Name of the Volume :param server: :class:`BoundServer `, None Server the Volume is attached to, None if it is not attached at all. :param created: datetime Point in time when the Volume was created :param location: :class:`BoundLocation ` Location of the Volume. Volume can only be attached to Servers in the same location. :param size: int Size in GB of the Volume :param linux_device: str Device path on the file system for the Volume :param protection: dict Protection configuration for the Volume :param labels: dict User-defined labels (key-value pairs) :param status: str Current status of the volume Choices: `creating`, `available` :param format: str, None Filesystem of the volume if formatted on creation, None if not formatted on creation. """ STATUS_CREATING = "creating" """Volume Status creating""" STATUS_AVAILABLE = "available" """Volume Status available""" __slots__ = ( "id", "name", "server", "location", "size", "linux_device", "format", "protection", "labels", "status", "created", ) def __init__( self, id, name=None, server=None, created=None, location=None, size=None, linux_device=None, format=None, protection=None, labels=None, status=None, ): self.id = id self.name = name self.server = server self.created = isoparse(created) if created else None self.location = location self.size = size self.linux_device = linux_device self.format = format self.protection = protection self.labels = labels self.status = status class CreateVolumeResponse(BaseDomain): """Create Volume Response Domain :param volume: :class:`BoundVolume ` The created volume :param action: :class:`BoundAction ` The action that shows the progress of the Volume Creation :param next_actions: List[:class:`BoundAction `] List of actions that are performed after the creation, like attaching to a server """ __slots__ = ("volume", "action", "next_actions") def __init__( self, volume, # type: BoundVolume action, # type: BoundAction next_actions, # type: List[BoundAction] ): self.volume = volume self.action = action self.next_actions = next_actions ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8425112 hcloud-1.16.0/hcloud.egg-info/0000755000175100001710000000000000000000000015365 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203244.0 hcloud-1.16.0/hcloud.egg-info/PKG-INFO0000644000175100001710000002175400000000000016473 0ustar00runnerdockerMetadata-Version: 2.1 Name: hcloud Version: 1.16.0 Summary: Official Hetzner Cloud python library Home-page: https://github.com/hetznercloud/hcloud-python Author: Hetzner Cloud GmbH Author-email: support-cloud@hetzner.com License: MIT license Description: Hetzner Cloud Python ==================== .. image:: https://github.com/hetznercloud/hcloud-python/workflows/Unit%20Tests/badge.svg :target: https://github.com/hetznercloud/hcloud-cloud-controller-manager/actions .. image:: https://github.com/hetznercloud/hcloud-python/workflows/Code%20Style/badge.svg :target: https://github.com/hetznercloud/hcloud-cloud-controller-manager/actions .. image:: https://readthedocs.org/projects/hcloud-python/badge/?version=latest :target: https://hcloud-python.readthedocs.io .. image:: https://img.shields.io/pypi/pyversions/hcloud.svg :target: https://pypi.org/project/hcloud/ Official Hetzner Cloud python library The library's documentation is available at `ReadTheDocs`_, the public API documentation is available at https://docs.hetzner.cloud. .. _ReadTheDocs: https://hcloud-python.readthedocs.io Usage example ------------- After the documentation has been created, click on `Usage` section Or open `docs/usage.rst` You can find some more examples under `examples/`. Supported Python versions ------------------------- We support python versions until `end-of-life`_. .. _end-of-life: https://devguide.python.org/#status-of-python-branches Development ----------- Setup Dev Environment --------------------- 1) `mkvirtualenv hcloud-python` 2) `pip install -e .` or `pip install -e .[docs]` to be able to build docs Run tests --------- * `tox .` * You can specify environment e.g `tox -e py36` * You can test the code style with `tox -e flake8` Create Documentation -------------------- Run `make docs`. This will also open a documentation in a tab in your default browser. Style Guide ------------- * **Type Hints**: If the type hint line is too long use inline hinting. Maximum inline type hint line should be 150 chars. License ------------- The MIT License (MIT). Please see `License File`_ for more information. .. _License File: https://github.com/hetznercloud/hcloud-python/blob/master/LICENSE ======= History ======= v1.16.0 (2021-08-17) --------------------- * Feature: Add support for Load Balancer DNS PTRs v1.15.0 (2021-08-16) --------------------- * Feature: Add support for Placement Groups v1.14.1 (2021-08-10) --------------------- * Bugfix: Fix crash on extra fields in public_net response * Improvement: Format code with black v1.14.0 (2021-08-03) --------------------- * Feature: Add support for Firewall rule descriptions v1.13.0 (2021-07-16) --------------------- * Feature: Add support for Firewall Protocols ESP and GRE * Feature: Add support for Image Type APP * Feature: Add support for creating Firewalls with Firewalls * Feature: Add support for Label Selectors in Firewalls * Improvement: Improve handling of underlying TCP connections. Now for every client instance a single TCP connection is used instead of one per call. * Note: Support for Python 2.7 and Python 3.5 was removed v1.12.0 (2021-04-06) --------------------- * Feature: Add support for managed Certificates v1.11.0 (2021-03-11) --------------------- * Feature: Add support for Firewalls * Feature: Add `primary_disk_size` to `Server` Domain v1.10.0 (2020-11-03) --------------------- * Feature: Add `include_deprecated` filter to `get_list` and `get_all` on `ImagesClient` * Feature: Add vSwitch support to `add_subnet` on `NetworksClient` * Feature: Add subnet type constants to `NetworkSubnet` domain (`NetworkSubnet.TYPE_CLOUD`, `NetworkSubnet.TYPE_VSWITCH`) v1.9.1 (2020-08-11) -------------------- * Bugfix: BoundLoadBalancer serialization failed when using IP targets v1.9.0 (2020-08-10) -------------------- * Feature: Add `included_traffic`, `outgoing_traffic` and `ingoing_traffic` properties to Load Balancer domain * Feature: Add `change_type`-method to `LoadBalancersClient` * Feature: Add support for `LoadBalancerTargetLabelSelector` * Feature: Add support for `LoadBalancerTargetLabelSelector` v1.8.2 (2020-07-20) -------------------- * Fix: Loosen up the requirements. v1.8.1 (2020-06-29) -------------------- * Fix Load Balancer Client. * Fix: Unify setting of request parameters within `get_list` methods. 1.8.0 (2020-06-22) -------------------- * Feature: Add Load Balancers **Attention: The Load Balancer support in v1.8.0 is kind of broken. Please use v1.8.1** * Feature: Add Certificates 1.7.1 (2020-06-15) -------------------- * Feature: Add requests 2.23 support 1.7.0 (2020-06-05) -------------------- * Feature: Add support for the optional 'networks' parameter on server creation. * Feature: Add python 3.9 support * Feature: Add subnet type `cloud` 1.6.3 (2020-01-09) -------------------- * Feature: Add 'created' property to SSH Key domain * Fix: Remove ISODatetime Descriptor because it leads to wrong dates 1.6.2 (2019-10-15) ------------------- * Fix: future dependency requirement was too strict 1.6.1 (2019-10-01) ------------------- * Fix: python-dateutil dependency requirement was too strict 1.6.0 (2019-09-17) ------------------- * Feature: Add missing `get_by_name` on `FloatingIPsClient` 1.5.0 (2019-09-16) ------------------- * Fix: ServersClient.create_image fails when specifying the `labels` * Feature: Add support for `name` on Floating IPs 1.4.1 (2019-08-19) ------------------ * Fix: Documentation for `NetworkRoute` domain was missing * Fix: `requests` dependency requirement was to strict 1.4.0 (2019-07-29) ------------------ * Feature: Add `mac_address` to Server PrivateNet domain * Feature: Add python 3.8 support 1.3.0 (2019-07-10) ------------------ * Feature: Add status filter for servers, images and volumes * Feature: Add 'created' property to Floating IP domain * Feature: Add 'Networks' support 1.2.1 (2019-03-13) ------------------ * Fix: BoundVolume.server server property now casted to the 'BoundServer'. 1.2.0 (2019-03-06) ------------------ * Feature: Add `get_by_fingerprint`-method for ssh keys * Fix: Create Floating IP with location raises an error because no action was given. 1.1.0 (2019-02-27) ------------------ * Feature: Add `STATUS`-constants for server and volume status 1.0.1 (2019-02-22) ------------------ Fix: Ignore unknown fields in API response instead of raising an error 1.0.0 (2019-02-21) ------------------ * First stable release. You can find the documentation under https://hcloud-python.readthedocs.io/en/latest/ 0.1.0 (2018-12-20) ------------------ * First release on GitHub. Keywords: hcloud hetzner cloud Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Requires-Python: !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11 Provides-Extra: docs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203244.0 hcloud-1.16.0/hcloud.egg-info/SOURCES.txt0000644000175100001710000001125400000000000017254 0ustar00runnerdockerCHANGELOG.rst CONTRIBUTING.rst LICENSE MANIFEST.in README.rst setup.cfg setup.py docs/Makefile docs/api.clients.actions.rst docs/api.clients.certificates.rst docs/api.clients.datacenters.rst docs/api.clients.firewalls.rst docs/api.clients.floating_ips.rst docs/api.clients.images.rst docs/api.clients.isos.rst docs/api.clients.load_balancer_types.rst docs/api.clients.load_balancers.rst docs/api.clients.locations.rst docs/api.clients.networks.rst docs/api.clients.placement_groups.rst docs/api.clients.server_types.rst docs/api.clients.servers.rst docs/api.clients.ssh_keys.rst docs/api.clients.volumes.rst docs/api.rst docs/changelog.rst docs/conf.py docs/contributing.rst docs/index.rst docs/installation.rst docs/make.bat docs/samples.rst hcloud/__init__.py hcloud/__version__.py hcloud/hcloud.py hcloud.egg-info/PKG-INFO hcloud.egg-info/SOURCES.txt hcloud.egg-info/dependency_links.txt hcloud.egg-info/not-zip-safe hcloud.egg-info/requires.txt hcloud.egg-info/top_level.txt hcloud/actions/__init__.py hcloud/actions/client.py hcloud/actions/domain.py hcloud/certificates/__init__.py hcloud/certificates/client.py hcloud/certificates/domain.py hcloud/core/__init__.py hcloud/core/client.py hcloud/core/domain.py hcloud/datacenters/__init__.py hcloud/datacenters/client.py hcloud/datacenters/domain.py hcloud/firewalls/__init__.py hcloud/firewalls/client.py hcloud/firewalls/domain.py hcloud/floating_ips/__init__.py hcloud/floating_ips/client.py hcloud/floating_ips/domain.py hcloud/helpers/__init__.py hcloud/images/__init__.py hcloud/images/client.py hcloud/images/domain.py hcloud/isos/__init__.py hcloud/isos/client.py hcloud/isos/domain.py hcloud/load_balancer_types/__init__.py hcloud/load_balancer_types/client.py hcloud/load_balancer_types/domain.py hcloud/load_balancers/__init__.py hcloud/load_balancers/client.py hcloud/load_balancers/domain.py hcloud/locations/__init__.py hcloud/locations/client.py hcloud/locations/domain.py hcloud/networks/__init__.py hcloud/networks/client.py hcloud/networks/domain.py hcloud/placement_groups/__init__.py hcloud/placement_groups/client.py hcloud/placement_groups/domain.py hcloud/server_types/__init__.py hcloud/server_types/client.py hcloud/server_types/domain.py hcloud/servers/__init__.py hcloud/servers/client.py hcloud/servers/domain.py hcloud/ssh_keys/__init__.py hcloud/ssh_keys/client.py hcloud/ssh_keys/domain.py hcloud/volumes/__init__.py hcloud/volumes/client.py hcloud/volumes/domain.py tests/__init__.py tests/unit/__init__.py tests/unit/conftest.py tests/unit/test_hcloud.py tests/unit/actions/__init__.py tests/unit/actions/conftest.py tests/unit/actions/test_client.py tests/unit/actions/test_domain.py tests/unit/certificates/__init__.py tests/unit/certificates/conftest.py tests/unit/certificates/test_client.py tests/unit/certificates/test_domain.py tests/unit/core/__init__.py tests/unit/core/test_client.py tests/unit/core/test_domain.py tests/unit/datacenters/__init__.py tests/unit/datacenters/conftest.py tests/unit/datacenters/test_client.py tests/unit/firewalls/__init__.py tests/unit/firewalls/conftest.py tests/unit/firewalls/test_client.py tests/unit/firewalls/test_domain.py tests/unit/floating_ips/__init__.py tests/unit/floating_ips/conftest.py tests/unit/floating_ips/test_client.py tests/unit/floating_ips/test_domain.py tests/unit/helpers/__init__.py tests/unit/images/__init__.py tests/unit/images/conftest.py tests/unit/images/test_client.py tests/unit/images/test_domain.py tests/unit/isos/__init__.py tests/unit/isos/conftest.py tests/unit/isos/test_client.py tests/unit/isos/test_domain.py tests/unit/load_balancer_types/__init__.py tests/unit/load_balancer_types/conftest.py tests/unit/load_balancer_types/test_client.py tests/unit/load_balancers/__init__.py tests/unit/load_balancers/conftest.py tests/unit/load_balancers/test_client.py tests/unit/load_balancers/test_domain.py tests/unit/locations/__init__.py tests/unit/locations/conftest.py tests/unit/locations/test_client.py tests/unit/networks/__init__.py tests/unit/networks/conftest.py tests/unit/networks/test_client.py tests/unit/networks/test_domain.py tests/unit/placement_groups/__init__.py tests/unit/placement_groups/conftest.py tests/unit/placement_groups/test_client.py tests/unit/placement_groups/test_domain.py tests/unit/server_types/__init__.py tests/unit/server_types/conftest.py tests/unit/server_types/test_client.py tests/unit/servers/__init__.py tests/unit/servers/conftest.py tests/unit/servers/test_client.py tests/unit/servers/test_domain.py tests/unit/ssh_keys/__init__.py tests/unit/ssh_keys/conftest.py tests/unit/ssh_keys/test_client.py tests/unit/ssh_keys/test_domain.py tests/unit/volumes/__init__.py tests/unit/volumes/conftest.py tests/unit/volumes/test_client.py tests/unit/volumes/test_domain.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203244.0 hcloud-1.16.0/hcloud.egg-info/dependency_links.txt0000644000175100001710000000000100000000000021433 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203244.0 hcloud-1.16.0/hcloud.egg-info/not-zip-safe0000644000175100001710000000000100000000000017613 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203244.0 hcloud-1.16.0/hcloud.egg-info/requires.txt0000644000175100001710000000015400000000000017765 0ustar00runnerdockerfuture<1,>=0.17.1 python-dateutil<3,>=2.7.5 requests<3,>=2.20 [docs] Sphinx==1.8.1 sphinx-rtd-theme==0.4.2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203244.0 hcloud-1.16.0/hcloud.egg-info/top_level.txt0000644000175100001710000000000700000000000020114 0ustar00runnerdockerhcloud ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8545113 hcloud-1.16.0/setup.cfg0000644000175100001710000000034400000000000014237 0ustar00runnerdocker[bdist_wheel] universal = 1 [pep8] ignore = E501,E722,W503 [flake8] ignore = E501,E722,W503 exclude = .git, docs, [aliases] test = pytest [tool:pytest] collect_ignore = ['setup.py'] [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/setup.py0000644000175100001710000000322000000000000014124 0ustar00runnerdocker#!/usr/bin/env python # -*- coding: utf-8 -*- """The setup script.""" from setuptools import setup, find_packages with open("README.rst") as readme_file: readme = readme_file.read() with open("CHANGELOG.rst") as changelog_file: changelog = changelog_file.read() requirements = ["future>=0.17.1,<1", "python-dateutil>=2.7.5,<3", "requests>=2.20,<3"] extras_require = {"docs": ["Sphinx==1.8.1", "sphinx-rtd-theme==0.4.2"]} version = {} with open("hcloud/__version__.py") as fp: exec(fp.read(), version) setup( author="Hetzner Cloud GmbH", author_email="support-cloud@hetzner.com", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], python_requires="!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11", description="Official Hetzner Cloud python library", install_requires=requirements, extras_require=extras_require, license="MIT license", long_description=readme + "\n\n" + changelog, include_package_data=True, keywords="hcloud hetzner cloud", name="hcloud", packages=find_packages(exclude=["examples", "tests*", "docs"]), test_suite="tests", url="https://github.com/hetznercloud/hcloud-python", version=version["VERSION"], zip_safe=False, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8465111 hcloud-1.16.0/tests/0000755000175100001710000000000000000000000013557 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/__init__.py0000644000175100001710000000000000000000000015656 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/0000755000175100001710000000000000000000000014536 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/__init__.py0000644000175100001710000000000000000000000016635 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/actions/0000755000175100001710000000000000000000000016176 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/actions/__init__.py0000644000175100001710000000000000000000000020275 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/actions/conftest.py0000644000175100001710000000440000000000000020373 0ustar00runnerdockerimport pytest @pytest.fixture() def generic_action_list(): return { "actions": [ { "id": 1, "command": "start_server", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, { "id": 2, "command": "stop_server", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, ] } @pytest.fixture() def running_action(): return { "action": { "id": 2, "command": "stop_server", "status": "running", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def successfully_action(): return { "action": { "id": 2, "command": "stop_server", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def failed_action(): return { "action": { "id": 2, "command": "stop_server", "status": "error", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/actions/test_client.py0000644000175100001710000001001600000000000021063 0ustar00runnerdockerimport mock import pytest from hcloud.actions.client import ActionsClient, BoundAction from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException class TestBoundAction(object): @pytest.fixture() def bound_running_action(self, mocked_requests): return BoundAction( client=ActionsClient(client=mocked_requests), data=dict(id=14, status=Action.STATUS_RUNNING), ) def test_wait_until_finished( self, bound_running_action, mocked_requests, running_action, successfully_action ): mocked_requests.request.side_effect = [running_action, successfully_action] bound_running_action.wait_until_finished() assert bound_running_action.status == "success" assert mocked_requests.request.call_count == 2 def test_wait_until_finished_with_error( self, bound_running_action, mocked_requests, running_action, failed_action ): mocked_requests.request.side_effect = [running_action, failed_action] with pytest.raises(ActionFailedException) as exception_info: bound_running_action.wait_until_finished() assert bound_running_action.status == "error" assert exception_info.value.action.id == 2 def test_wait_until_finished_max_retries( self, bound_running_action, mocked_requests, running_action, successfully_action ): mocked_requests.request.side_effect = [ running_action, running_action, successfully_action, ] with pytest.raises(ActionTimeoutException) as exception_info: bound_running_action.wait_until_finished(max_retries=1) assert bound_running_action.status == "running" assert exception_info.value.action.id == 2 assert mocked_requests.request.call_count == 1 class TestActionsClient(object): @pytest.fixture() def actions_client(self): return ActionsClient(client=mock.MagicMock()) def test_get_by_id(self, actions_client, generic_action): actions_client._client.request.return_value = generic_action action = actions_client.get_by_id(1) actions_client._client.request.assert_called_with( url="/actions/1", method="GET" ) assert action._client is actions_client assert action.id == 1 assert action.command == "stop_server" @pytest.mark.parametrize( "params", [{}, {"status": ["active"], "sort": ["status"], "page": 2, "per_page": 10}], ) def test_get_list(self, actions_client, generic_action_list, params): actions_client._client.request.return_value = generic_action_list result = actions_client.get_list(**params) actions_client._client.request.assert_called_with( url="/actions", method="GET", params=params ) assert result.meta is None actions = result.actions assert len(actions) == 2 action1 = actions[0] action2 = actions[1] assert action1._client is actions_client assert action1.id == 1 assert action1.command == "start_server" assert action2._client is actions_client assert action2.id == 2 assert action2.command == "stop_server" @pytest.mark.parametrize("params", [{}, {"status": ["active"], "sort": ["status"]}]) def test_get_all(self, actions_client, generic_action_list, params): actions_client._client.request.return_value = generic_action_list actions = actions_client.get_all(**params) params.update({"page": 1, "per_page": 50}) actions_client._client.request.assert_called_with( url="/actions", method="GET", params=params ) assert len(actions) == 2 action1 = actions[0] action2 = actions[1] assert action1._client is actions_client assert action1.id == 1 assert action1.command == "start_server" assert action2._client is actions_client assert action2.id == 2 assert action2.command == "stop_server" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/actions/test_domain.py0000644000175100001710000000102200000000000021051 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.actions.domain import Action class TestAction(object): def test_started_finished_is_datetime(self): action = Action( id=1, started="2016-01-30T23:50+00:00", finished="2016-03-30T23:50+00:00" ) assert action.started == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) assert action.finished == datetime.datetime( 2016, 3, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/certificates/0000755000175100001710000000000000000000000017203 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/certificates/__init__.py0000644000175100001710000000000000000000000021302 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/certificates/conftest.py0000644000175100001710000001426000000000000021405 0ustar00runnerdockerimport pytest @pytest.fixture() def certificate_response(): return { "certificate": { "id": 2323, "name": "My Certificate", "type": "managed", "labels": {}, "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": ["example.com", "webmail.example.com", "www.example.com"], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": { "issuance": "failed", "renewal": "scheduled", "error": {"code": "error_code", "message": "error message"}, }, "used_by": [{"id": 42, "type": "server"}], } } @pytest.fixture() def create_managed_certificate_response(): return { "certificate": { "id": 2323, "name": "My Certificate", "type": "managed", "labels": {}, "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": ["example.com", "webmail.example.com", "www.example.com"], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": {"issuance": "pending", "renewal": "scheduled", "error": None}, "used_by": [{"id": 42, "type": "load_balancer"}], }, "action": { "id": 14, "command": "issue_certificate", "status": "success", "progress": 100, "started": "2021-01-30T23:55:00+00:00", "finished": "2021-01-30T23:57:00+00:00", "resources": [{"id": 896, "type": "certificate"}], "error": {"code": "action_failed", "message": "Action failed"}, }, } @pytest.fixture() def two_certificates_response(): return { "certificates": [ { "id": 2323, "name": "My Certificate", "labels": {}, "type": "uploaded", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": [ "example.com", "webmail.example.com", "www.example.com", ], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": None, "used_by": [{"id": 42, "type": "load_balancer"}], }, { "id": 2324, "name": "My website cert", "labels": {}, "type": "uploaded", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": [ "example.com", "webmail.example.com", "www.example.com", ], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": None, "used_by": [{"id": 42, "type": "load_balancer"}], }, ] } @pytest.fixture() def one_certificates_response(): return { "certificates": [ { "id": 2323, "name": "My Certificate", "labels": {}, "type": "uploaded", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": [ "example.com", "webmail.example.com", "www.example.com", ], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": None, "used_by": [{"id": 42, "type": "load_balancer"}], } ] } @pytest.fixture() def response_update_certificate(): return { "certificate": { "id": 2323, "name": "New name", "labels": {}, "type": "uploaded", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": ["example.com", "webmail.example.com", "www.example.com"], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": None, "used_by": [{"id": 42, "type": "load_balancer"}], } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "change_protection", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 14, "type": "certificate"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } @pytest.fixture() def response_retry_issuance_action(): return { "action": { "id": 14, "command": "issue_certificate", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "certificate"}], "error": {"code": "action_failed", "message": "Action failed"}, } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/certificates/test_client.py0000644000175100001710000002551300000000000022100 0ustar00runnerdockerimport pytest import mock from hcloud.actions.client import BoundAction from hcloud.certificates.client import CertificatesClient, BoundCertificate from hcloud.certificates.domain import Certificate, ManagedCertificateStatus class TestBoundCertificate(object): @pytest.fixture() def bound_certificate(self, hetzner_client): return BoundCertificate(client=hetzner_client.certificates, data=dict(id=14)) @pytest.mark.parametrize("params", [{"page": 1, "per_page": 10}, {}]) def test_get_actions_list( self, hetzner_client, bound_certificate, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_certificate.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/certificates/14/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_get_actions(self, hetzner_client, bound_certificate, response_get_actions): hetzner_client.request.return_value = response_get_actions actions = bound_certificate.get_actions() params = {"page": 1, "per_page": 50} hetzner_client.request.assert_called_with( url="/certificates/14/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_bound_certificate_init(self, certificate_response): bound_certificate = BoundCertificate( client=mock.MagicMock(), data=certificate_response["certificate"] ) assert bound_certificate.id == 2323 assert bound_certificate.name == "My Certificate" assert bound_certificate.type == "managed" assert ( bound_certificate.fingerprint == "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f" ) assert bound_certificate.certificate == "-----BEGIN CERTIFICATE-----\n..." assert len(bound_certificate.domain_names) == 3 assert bound_certificate.domain_names[0] == "example.com" assert bound_certificate.domain_names[1] == "webmail.example.com" assert bound_certificate.domain_names[2] == "www.example.com" assert isinstance(bound_certificate.status, ManagedCertificateStatus) assert bound_certificate.status.issuance == "failed" assert bound_certificate.status.renewal == "scheduled" assert bound_certificate.status.error.code == "error_code" assert bound_certificate.status.error.message == "error message" def test_update( self, hetzner_client, bound_certificate, response_update_certificate ): hetzner_client.request.return_value = response_update_certificate certificate = bound_certificate.update(name="New name") hetzner_client.request.assert_called_with( url="/certificates/14", method="PUT", json={"name": "New name"} ) assert certificate.id == 2323 assert certificate.name == "New name" def test_delete(self, hetzner_client, bound_certificate, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_certificate.delete() hetzner_client.request.assert_called_with( url="/certificates/14", method="DELETE" ) assert delete_success is True def test_retry_issuance( self, hetzner_client, bound_certificate, response_retry_issuance_action ): hetzner_client.request.return_value = response_retry_issuance_action action = bound_certificate.retry_issuance() hetzner_client.request.assert_called_with( url="/certificates/14/actions/retry", method="POST" ) assert action.id == 14 assert action.command == "issue_certificate" class TestCertificatesClient(object): @pytest.fixture() def certificates_client(self): return CertificatesClient(client=mock.MagicMock()) def test_get_by_id(self, certificates_client, certificate_response): certificates_client._client.request.return_value = certificate_response certificate = certificates_client.get_by_id(1) certificates_client._client.request.assert_called_with( url="/certificates/1", method="GET" ) assert certificate._client is certificates_client assert certificate.id == 2323 assert certificate.name == "My Certificate" @pytest.mark.parametrize( "params", [ { "name": "My Certificate", "label_selector": "k==v", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list(self, certificates_client, two_certificates_response, params): certificates_client._client.request.return_value = two_certificates_response result = certificates_client.get_list(**params) certificates_client._client.request.assert_called_with( url="/certificates", method="GET", params=params ) certificates = result.certificates assert len(certificates) == 2 certificates1 = certificates[0] certificates2 = certificates[1] assert certificates1._client is certificates_client assert certificates1.id == 2323 assert certificates1.name == "My Certificate" assert certificates2._client is certificates_client assert certificates2.id == 2324 assert certificates2.name == "My website cert" @pytest.mark.parametrize( "params", [{"name": "My Certificate", "label_selector": "label1"}, {}] ) def test_get_all(self, certificates_client, two_certificates_response, params): certificates_client._client.request.return_value = two_certificates_response certificates = certificates_client.get_all(**params) params.update({"page": 1, "per_page": 50}) certificates_client._client.request.assert_called_with( url="/certificates", method="GET", params=params ) assert len(certificates) == 2 certificates1 = certificates[0] certificates2 = certificates[1] assert certificates1._client is certificates_client assert certificates1.id == 2323 assert certificates1.name == "My Certificate" assert certificates2._client is certificates_client assert certificates2.id == 2324 assert certificates2.name == "My website cert" def test_get_by_name(self, certificates_client, one_certificates_response): certificates_client._client.request.return_value = one_certificates_response certificates = certificates_client.get_by_name("My Certificate") params = {"name": "My Certificate"} certificates_client._client.request.assert_called_with( url="/certificates", method="GET", params=params ) assert certificates._client is certificates_client assert certificates.id == 2323 assert certificates.name == "My Certificate" def test_create(self, certificates_client, certificate_response): certificates_client._client.request.return_value = certificate_response certificate = certificates_client.create( name="My Certificate", certificate="-----BEGIN CERTIFICATE-----\n...", private_key="-----BEGIN PRIVATE KEY-----\n...", ) certificates_client._client.request.assert_called_with( url="/certificates", method="POST", json={ "name": "My Certificate", "certificate": "-----BEGIN CERTIFICATE-----\n...", "private_key": "-----BEGIN PRIVATE KEY-----\n...", "type": "uploaded", }, ) assert certificate.id == 2323 assert certificate.name == "My Certificate" def test_create_managed( self, certificates_client, create_managed_certificate_response ): certificates_client._client.request.return_value = ( create_managed_certificate_response ) create_managed_certificate_rsp = certificates_client.create_managed( name="My Certificate", domain_names=["example.com", "*.example.org"] ) certificates_client._client.request.assert_called_with( url="/certificates", method="POST", json={ "name": "My Certificate", "domain_names": ["example.com", "*.example.org"], "type": "managed", }, ) assert create_managed_certificate_rsp.certificate.id == 2323 assert create_managed_certificate_rsp.certificate.name == "My Certificate" assert create_managed_certificate_rsp.action.id == 14 assert create_managed_certificate_rsp.action.command == "issue_certificate" @pytest.mark.parametrize( "certificate", [Certificate(id=1), BoundCertificate(mock.MagicMock(), dict(id=1))], ) def test_update( self, certificates_client, certificate, response_update_certificate ): certificates_client._client.request.return_value = response_update_certificate certificate = certificates_client.update(certificate, name="New name") certificates_client._client.request.assert_called_with( url="/certificates/1", method="PUT", json={"name": "New name"} ) assert certificate.id == 2323 assert certificate.name == "New name" @pytest.mark.parametrize( "certificate", [Certificate(id=1), BoundCertificate(mock.MagicMock(), dict(id=1))], ) def test_delete(self, certificates_client, certificate, generic_action): certificates_client._client.request.return_value = generic_action delete_success = certificates_client.delete(certificate) certificates_client._client.request.assert_called_with( url="/certificates/1", method="DELETE" ) assert delete_success is True @pytest.mark.parametrize( "certificate", [Certificate(id=1), BoundCertificate(mock.MagicMock(), dict(id=1))], ) def test_retry_issuance( self, certificates_client, certificate, response_retry_issuance_action ): certificates_client._client.request.return_value = ( response_retry_issuance_action ) action = certificates_client.retry_issuance(certificate) certificates_client._client.request.assert_called_with( url="/certificates/1/actions/retry", method="POST" ) assert action.id == 14 assert action.command == "issue_certificate" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/certificates/test_domain.py0000644000175100001710000000142000000000000022060 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.certificates.domain import Certificate class TestCertificate(object): def test_created_is_datetime(self): certificate = Certificate( id=1, created="2016-01-30T23:50+00:00", not_valid_after="2016-01-30T23:50+00:00", not_valid_before="2016-01-30T23:50+00:00", ) assert certificate.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) assert certificate.not_valid_after == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) assert certificate.not_valid_before == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/conftest.py0000644000175100001710000000154100000000000016736 0ustar00runnerdockerimport mock import pytest from hcloud import Client @pytest.fixture(autouse=True, scope="function") def mocked_requests(): patcher = mock.patch("hcloud.hcloud.requests") mocked_requests = patcher.start() yield mocked_requests patcher.stop() @pytest.fixture() def generic_action(): return { "action": { "id": 1, "command": "stop_server", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def hetzner_client(): client = Client(token="token") patcher = mock.patch.object(client, "request") patcher.start() yield client patcher.stop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/core/0000755000175100001710000000000000000000000015466 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/core/__init__.py0000644000175100001710000000000000000000000017565 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/core/test_client.py0000644000175100001710000002173500000000000020365 0ustar00runnerdockerimport mock import pytest from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result, BaseDomain class TestBoundModelBase: @pytest.fixture() def bound_model_class(self): class Model(BaseDomain): __slots__ = ("id", "name", "description") def __init__(self, id, name="", description=""): self.id = id self.name = name self.description = description class BoundModel(BoundModelBase): model = Model return BoundModel @pytest.fixture() def client(self): client = mock.MagicMock() return client def test_get_exists_model_attribute_complete_model(self, bound_model_class, client): bound_model = bound_model_class( client=client, data={"id": 1, "name": "name", "description": "my_description"}, ) description = bound_model.description client.get_by_id.assert_not_called() assert description == "my_description" def test_get_non_exists_model_attribute_complete_model( self, bound_model_class, client ): bound_model = bound_model_class( client=client, data={"id": 1, "name": "name", "description": "description"} ) with pytest.raises(AttributeError): bound_model.content client.get_by_id.assert_not_called() def test_get_exists_model_attribute_incomplete_model( self, bound_model_class, client ): bound_model = bound_model_class(client=client, data={"id": 101}, complete=False) client.get_by_id.return_value = bound_model_class( client=client, data={"id": 101, "name": "name", "description": "super_description"}, ) description = bound_model.description client.get_by_id.assert_called_once_with(101) assert description == "super_description" assert bound_model.complete is True def test_get_filled_model_attribute_incomplete_model( self, bound_model_class, client ): bound_model = bound_model_class(client=client, data={"id": 101}, complete=False) id = bound_model.id client.get_by_id.assert_not_called() assert id == 101 assert bound_model.complete is False def test_get_non_exists_model_attribute_incomplete_model( self, bound_model_class, client ): bound_model = bound_model_class(client=client, data={"id": 1}, complete=False) with pytest.raises(AttributeError): bound_model.content client.get_by_id.assert_not_called() assert bound_model.complete is False class TestClientEntityBase: @pytest.fixture() def client_class_constructor(self): def constructor(json_content_function): class CandiesClient(ClientEntityBase): results_list_attribute_name = "candies" def get_list(self, status, page=None, per_page=None): json_content = json_content_function(page) results = [ (r, page, status, per_page) for r in json_content["candies"] ] return self._add_meta_to_result(results, json_content) return CandiesClient(mock.MagicMock()) return constructor @pytest.fixture() def client_class_with_actions_constructor(self): def constructor(json_content_function): class CandiesClient(ClientEntityBase): def get_actions_list(self, status, page=None, per_page=None): json_content = json_content_function(page) results = [ (r, page, status, per_page) for r in json_content["actions"] ] return add_meta_to_result(results, json_content, "actions") return CandiesClient(mock.MagicMock()) return constructor def test_get_all_no_meta(self, client_class_constructor): json_content = {"candies": [1, 2]} def json_content_function(p): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client.get_all(status="sweet") assert result == [(1, 1, "sweet", 50), (2, 1, "sweet", 50)] def test_get_all_no_next_page(self, client_class_constructor): json_content = { "candies": [1, 2], "meta": {"pagination": {"page": 1, "per_page": 11, "next_page": None}}, } def json_content_function(p): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client.get_all(status="sweet") assert result == [(1, 1, "sweet", 50), (2, 1, "sweet", 50)] def test_get_all_ok(self, client_class_constructor): def json_content_function(p): return { "candies": [10 + p, 20 + p], "meta": { "pagination": { "page": p, "per_page": 11, "next_page": p + 1 if p < 3 else None, } }, } candies_client = client_class_constructor(json_content_function) result = candies_client.get_all(status="sweet") assert result == [ (11, 1, "sweet", 50), (21, 1, "sweet", 50), (12, 2, "sweet", 50), (22, 2, "sweet", 50), (13, 3, "sweet", 50), (23, 3, "sweet", 50), ] def test_get_actions_no_method(self, client_class_constructor): json_content = {"candies": [1, 2]} def json_content_function(p): return json_content candies_client = client_class_constructor(json_content_function) with pytest.raises(ValueError) as exception_info: candies_client.get_actions() error = exception_info.value assert str(error) == "this endpoint does not support get_actions method" def test_get_actions_ok(self, client_class_with_actions_constructor): def json_content_function(p): return { "actions": [10 + p, 20 + p], "meta": { "pagination": { "page": p, "per_page": 11, "next_page": p + 1 if p < 3 else None, } }, } candies_client = client_class_with_actions_constructor(json_content_function) result = candies_client.get_actions(status="sweet") assert result == [ (11, 1, "sweet", 50), (21, 1, "sweet", 50), (12, 2, "sweet", 50), (22, 2, "sweet", 50), (13, 3, "sweet", 50), (23, 3, "sweet", 50), ] def test_raise_exception_if_list_attribute_is_not_implemented( self, client_class_with_actions_constructor ): def json_content_function(p): return { "actions": [10 + p, 20 + p], "meta": { "pagination": { "page": p, "per_page": 11, "next_page": p + 1 if p < 3 else None, } }, } candies_client = client_class_with_actions_constructor(json_content_function) with pytest.raises(NotImplementedError) as exception_info: candies_client.get_all() error = exception_info.value assert ( str(error) == "in order to get results list, 'results_list_attribute_name' attribute of CandiesClient has to be specified" ) class TestGetEntityByNameMixin: @pytest.fixture() def client_class_constructor(self): def constructor(json_content_function): class CandiesClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "candies" def get_list(self, name, page=None, per_page=None): json_content = json_content_function(page) results = json_content["candies"] return self._add_meta_to_result(results, json_content) return CandiesClient(mock.MagicMock()) return constructor def test_get_by_name_result_exists(self, client_class_constructor): json_content = {"candies": [1]} def json_content_function(p): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client.get_by_name(name="sweet") assert result == 1 def test_get_by_name_result_does_not_exist(self, client_class_constructor): json_content = {"candies": []} def json_content_function(p): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client.get_by_name(name="sweet") assert result is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/core/test_domain.py0000644000175100001710000001027700000000000020355 0ustar00runnerdockerimport pytest from dateutil.parser import isoparse from hcloud.core.domain import ( BaseDomain, DomainIdentityMixin, Meta, Pagination, add_meta_to_result, ) class TestMeta(object): @pytest.mark.parametrize("json_content", [None, "", {}]) def test_parse_meta_empty_json(self, json_content): result = Meta.parse_meta(json_content) assert result is None def test_parse_meta_json_no_paginaton(self): json_content = {"meta": {}} result = Meta.parse_meta(json_content) assert isinstance(result, Meta) assert result.pagination is None def test_parse_meta_json_ok(self): json_content = { "meta": { "pagination": { "page": 2, "per_page": 10, "previous_page": 1, "next_page": 3, "last_page": 10, "total_entries": 100, } } } result = Meta.parse_meta(json_content) assert isinstance(result, Meta) assert isinstance(result.pagination, Pagination) assert result.pagination.page == 2 assert result.pagination.per_page == 10 assert result.pagination.next_page == 3 assert result.pagination.last_page == 10 assert result.pagination.total_entries == 100 def test_add_meta_to_result(self): json_content = { "meta": { "pagination": { "page": 2, "per_page": 10, "previous_page": 1, "next_page": 3, "last_page": 10, "total_entries": 100, } } } result = add_meta_to_result([1, 2, 3], json_content, "id_list") assert result.id_list == [1, 2, 3] assert result.meta.pagination.page == 2 assert result.meta.pagination.per_page == 10 assert result.meta.pagination.next_page == 3 assert result.meta.pagination.last_page == 10 assert result.meta.pagination.total_entries == 100 class SomeDomain(BaseDomain, DomainIdentityMixin): __slots__ = ("id", "name") def __init__(self, id=None, name=None): self.id = id self.name = name class TestDomainIdentityMixin(object): @pytest.mark.parametrize( "domain,expected_result", [ (SomeDomain(id=1, name="name"), 1), (SomeDomain(id=1), 1), (SomeDomain(name="name"), "name"), ], ) def test_id_or_name_ok(self, domain, expected_result): assert domain.id_or_name == expected_result def test_id_or_name_exception(self): domain = SomeDomain() with pytest.raises(ValueError) as exception_info: domain.id_or_name error = exception_info.value assert str(error) == "id or name must be set" class ActionDomain(BaseDomain, DomainIdentityMixin): __slots__ = ("id", "name", "started") def __init__(self, id, name="name1", started=None): self.id = id self.name = name self.started = isoparse(started) if started else None class TestBaseDomain(object): @pytest.mark.parametrize( "data_dict,expected_result", [ ({"id": 1}, {"id": 1, "name": "name1", "started": None}), ({"id": 2, "name": "name2"}, {"id": 2, "name": "name2", "started": None}), ( {"id": 3, "foo": "boo", "description": "new"}, {"id": 3, "name": "name1", "started": None}, ), ( { "id": 4, "foo": "boo", "description": "new", "name": "name-name3", "started": "2016-01-30T23:50+00:00", }, { "id": 4, "name": "name-name3", "started": isoparse("2016-01-30T23:50+00:00"), }, ), ], ) def test_from_dict_ok(self, data_dict, expected_result): model = ActionDomain.from_dict(data_dict) for k, v in expected_result.items(): assert getattr(model, k) == v ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/datacenters/0000755000175100001710000000000000000000000017033 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/datacenters/__init__.py0000644000175100001710000000000000000000000021132 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/datacenters/conftest.py0000644000175100001710000000570300000000000021237 0ustar00runnerdockerimport pytest @pytest.fixture() def datacenter_response(): return { "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, } } @pytest.fixture() def two_datacenters_response(): return { "datacenters": [ { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, { "id": 2, "name": "nbg1-dc3", "description": "Nuremberg 1 DC 3", "location": { "id": 2, "name": "nbg1", "description": "Nuremberg DC Park 1", "country": "DE", "city": "Nuremberg", "latitude": 49.452102, "longitude": 11.076665, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, ], "recommendation": 1, } @pytest.fixture() def one_datacenters_response(): return { "datacenters": [ { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, } ], "recommendation": 1, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/datacenters/test_client.py0000644000175100001710000001341300000000000021724 0ustar00runnerdockerimport pytest # noqa: F401 import mock # noqa: F401 from hcloud.datacenters.client import DatacentersClient, BoundDatacenter from hcloud.datacenters.domain import DatacenterServerTypes from hcloud.locations.client import BoundLocation class TestBoundDatacenter(object): def test_bound_datacenter_init(self, datacenter_response): bound_datacenter = BoundDatacenter( client=mock.MagicMock(), data=datacenter_response["datacenter"] ) assert bound_datacenter.id == 1 assert bound_datacenter.name == "fsn1-dc8" assert bound_datacenter.description == "Falkenstein 1 DC 8" assert bound_datacenter.complete is True assert isinstance(bound_datacenter.location, BoundLocation) assert bound_datacenter.location.id == 1 assert bound_datacenter.location.name == "fsn1" assert bound_datacenter.location.complete is True assert isinstance(bound_datacenter.server_types, DatacenterServerTypes) assert len(bound_datacenter.server_types.supported) == 3 assert bound_datacenter.server_types.supported[0].id == 1 assert bound_datacenter.server_types.supported[0].complete is False assert bound_datacenter.server_types.supported[1].id == 2 assert bound_datacenter.server_types.supported[1].complete is False assert bound_datacenter.server_types.supported[2].id == 3 assert bound_datacenter.server_types.supported[2].complete is False assert len(bound_datacenter.server_types.available) == 3 assert bound_datacenter.server_types.available[0].id == 1 assert bound_datacenter.server_types.available[0].complete is False assert bound_datacenter.server_types.available[1].id == 2 assert bound_datacenter.server_types.available[1].complete is False assert bound_datacenter.server_types.available[2].id == 3 assert bound_datacenter.server_types.available[2].complete is False assert len(bound_datacenter.server_types.available_for_migration) == 3 assert bound_datacenter.server_types.available_for_migration[0].id == 1 assert ( bound_datacenter.server_types.available_for_migration[0].complete is False ) assert bound_datacenter.server_types.available_for_migration[1].id == 2 assert ( bound_datacenter.server_types.available_for_migration[1].complete is False ) assert bound_datacenter.server_types.available_for_migration[2].id == 3 assert ( bound_datacenter.server_types.available_for_migration[2].complete is False ) class TestDatacentersClient(object): @pytest.fixture() def datacenters_client(self): return DatacentersClient(client=mock.MagicMock()) def test_get_by_id(self, datacenters_client, datacenter_response): datacenters_client._client.request.return_value = datacenter_response datacenter = datacenters_client.get_by_id(1) datacenters_client._client.request.assert_called_with( url="/datacenters/1", method="GET" ) assert datacenter._client is datacenters_client assert datacenter.id == 1 assert datacenter.name == "fsn1-dc8" @pytest.mark.parametrize( "params", [{"name": "fsn1", "page": 1, "per_page": 10}, {"name": ""}, {}] ) def test_get_list(self, datacenters_client, two_datacenters_response, params): datacenters_client._client.request.return_value = two_datacenters_response result = datacenters_client.get_list(**params) datacenters_client._client.request.assert_called_with( url="/datacenters", method="GET", params=params ) datacenters = result.datacenters assert result.meta is None assert len(datacenters) == 2 datacenter1 = datacenters[0] datacenter2 = datacenters[1] assert datacenter1._client is datacenters_client assert datacenter1.id == 1 assert datacenter1.name == "fsn1-dc8" assert isinstance(datacenter1.location, BoundLocation) assert datacenter2._client is datacenters_client assert datacenter2.id == 2 assert datacenter2.name == "nbg1-dc3" assert isinstance(datacenter2.location, BoundLocation) @pytest.mark.parametrize("params", [{"name": "fsn1"}, {}]) def test_get_all(self, datacenters_client, two_datacenters_response, params): datacenters_client._client.request.return_value = two_datacenters_response datacenters = datacenters_client.get_all(**params) params.update({"page": 1, "per_page": 50}) datacenters_client._client.request.assert_called_with( url="/datacenters", method="GET", params=params ) assert len(datacenters) == 2 datacenter1 = datacenters[0] datacenter2 = datacenters[1] assert datacenter1._client is datacenters_client assert datacenter1.id == 1 assert datacenter1.name == "fsn1-dc8" assert isinstance(datacenter1.location, BoundLocation) assert datacenter2._client is datacenters_client assert datacenter2.id == 2 assert datacenter2.name == "nbg1-dc3" assert isinstance(datacenter2.location, BoundLocation) def test_get_by_name(self, datacenters_client, one_datacenters_response): datacenters_client._client.request.return_value = one_datacenters_response datacenter = datacenters_client.get_by_name("fsn1-dc8") params = {"name": "fsn1-dc8"} datacenters_client._client.request.assert_called_with( url="/datacenters", method="GET", params=params ) assert datacenter._client is datacenters_client assert datacenter.id == 1 assert datacenter.name == "fsn1-dc8" assert isinstance(datacenter.location, BoundLocation) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/firewalls/0000755000175100001710000000000000000000000016526 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/firewalls/__init__.py0000644000175100001710000000000000000000000020625 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/firewalls/conftest.py0000644000175100001710000002137600000000000020736 0ustar00runnerdockerimport pytest @pytest.fixture() def response_create_firewall(): return { "firewall": { "id": 38, "name": "Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "destination_ips": [], "protocol": "tcp", "port": "80", "description": None, }, { "direction": "out", "source_ips": [], "destination_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "protocol": "tcp", "port": "80", "description": "allow http out", }, ], "applied_to": [ {"server": {"id": 42}, "type": "server"}, { "type": "label_selector", "label_selector": {"selector": "key==value"}, }, ], }, "actions": [ { "command": "set_firewall_rules", "error": {"code": "action_failed", "message": "Action failed"}, "finished": "2016-01-30T23:56:00+00:00", "id": 13, "progress": 100, "resources": [{"id": 38, "type": "firewall"}], "started": "2016-01-30T23:55:00+00:00", "status": "success", }, { "command": "apply_firewall", "error": {"code": "action_failed", "message": "Action failed"}, "finished": "2016-01-30T23:56:00+00:00", "id": 14, "progress": 100, "resources": [ {"id": 42, "type": "server"}, {"id": 38, "type": "firewall"}, ], "started": "2016-01-30T23:55:00+00:00", "status": "success", }, ], } @pytest.fixture() def firewall_response(): return { "firewall": { "id": 38, "name": "Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "destination_ips": [], "protocol": "tcp", "port": "80", "description": "allow http in", }, { "direction": "out", "source_ips": [], "destination_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "protocol": "tcp", "port": "80", "description": "allow http out", }, ], "applied_to": [ {"server": {"id": 42}, "type": "server"}, { "type": "label_selector", "label_selector": {"selector": "key==value"}, }, ], } } @pytest.fixture() def two_firewalls_response(): return { "firewalls": [ { "id": 38, "name": "Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "destination_ips": [], "protocol": "tcp", "port": "80", "description": "allow http in", } ], "applied_to": [{"server": {"id": 42}, "type": "server"}], }, { "id": 39, "name": "Corporate Extranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "destination_ips": [], "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "protocol": "tcp", "port": "443", "description": "allow https in", } ], "applied_to": [{"server": {"id": 42}, "type": "server"}], }, ] } @pytest.fixture() def one_firewalls_response(): return { "firewalls": [ { "id": 38, "name": "Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "destination_ips": [], "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "protocol": "tcp", "port": "80", "description": "allow http in", } ], "applied_to": [{"server": {"id": 42}, "type": "server"}], }, ] } @pytest.fixture() def response_update_firewall(): return { "firewall": { "id": 38, "name": "New Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "destination_ips": [], "protocol": "tcp", "port": "80", "description": "allow http in", } ], "applied_to": [{"server": {"id": 42}, "type": "server"}], } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "set_firewall_rules", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "firewall"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } @pytest.fixture() def response_set_rules(): return { "actions": [ { "id": 13, "command": "set_firewall_rules", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 38, "type": "firewall"}], "error": {"code": "action_failed", "message": "Action failed"}, }, { "id": 14, "command": "apply_firewall", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [ {"id": 38, "type": "firewall"}, {"id": 42, "type": "server"}, ], "error": {"code": "action_failed", "message": "Action failed"}, }, ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/firewalls/test_client.py0000644000175100001710000004142700000000000021425 0ustar00runnerdockerimport pytest import mock from hcloud.firewalls.client import FirewallsClient, BoundFirewall from hcloud.actions.client import BoundAction from hcloud.firewalls.domain import ( Firewall, FirewallRule, FirewallResource, FirewallResourceLabelSelector, ) from hcloud.servers.domain import Server class TestBoundFirewall(object): @pytest.fixture() def bound_firewall(self, hetzner_client): return BoundFirewall(client=hetzner_client.firewalls, data=dict(id=1)) def test_bound_firewall_init(self, firewall_response): bound_firewall = BoundFirewall( client=mock.MagicMock(), data=firewall_response["firewall"] ) assert bound_firewall.id == 38 assert bound_firewall.name == "Corporate Intranet Protection" assert bound_firewall.labels == {} assert isinstance(bound_firewall.rules, list) assert len(bound_firewall.rules) == 2 assert isinstance(bound_firewall.applied_to, list) assert len(bound_firewall.applied_to) == 2 assert bound_firewall.applied_to[0].server.id == 42 assert bound_firewall.applied_to[0].type == "server" assert bound_firewall.applied_to[1].label_selector.selector == "key==value" assert bound_firewall.applied_to[1].type == "label_selector" firewall_in_rule = bound_firewall.rules[0] assert isinstance(firewall_in_rule, FirewallRule) assert firewall_in_rule.direction == FirewallRule.DIRECTION_IN assert firewall_in_rule.protocol == FirewallRule.PROTOCOL_TCP assert firewall_in_rule.port == "80" assert isinstance(firewall_in_rule.source_ips, list) assert len(firewall_in_rule.source_ips) == 3 assert firewall_in_rule.source_ips == [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ] assert isinstance(firewall_in_rule.destination_ips, list) assert len(firewall_in_rule.destination_ips) == 0 assert firewall_in_rule.description == "allow http in" firewall_out_rule = bound_firewall.rules[1] assert isinstance(firewall_out_rule, FirewallRule) assert firewall_out_rule.direction == FirewallRule.DIRECTION_OUT assert firewall_out_rule.protocol == FirewallRule.PROTOCOL_TCP assert firewall_out_rule.port == "80" assert isinstance(firewall_out_rule.source_ips, list) assert len(firewall_out_rule.source_ips) == 0 assert isinstance(firewall_out_rule.destination_ips, list) assert len(firewall_out_rule.destination_ips) == 3 assert firewall_out_rule.destination_ips == [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ] assert firewall_out_rule.description == "allow http out" @pytest.mark.parametrize( "params", [{}, {"sort": ["created"], "page": 1, "per_page": 2}] ) def test_get_actions_list( self, hetzner_client, bound_firewall, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_firewall.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/firewalls/1/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" @pytest.mark.parametrize("params", [{}, {"sort": ["created"]}]) def test_get_actions( self, hetzner_client, bound_firewall, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions actions = bound_firewall.get_actions(**params) params.update({"page": 1, "per_page": 50}) hetzner_client.request.assert_called_with( url="/firewalls/1/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" def test_update(self, hetzner_client, bound_firewall, response_update_firewall): hetzner_client.request.return_value = response_update_firewall firewall = bound_firewall.update( name="New Corporate Intranet Protection", labels={} ) hetzner_client.request.assert_called_with( url="/firewalls/1", method="PUT", json={"name": "New Corporate Intranet Protection", "labels": {}}, ) assert firewall.id == 38 assert firewall.name == "New Corporate Intranet Protection" def test_delete(self, hetzner_client, bound_firewall): delete_success = bound_firewall.delete() hetzner_client.request.assert_called_with(url="/firewalls/1", method="DELETE") assert delete_success is True def test_set_rules(self, hetzner_client, bound_firewall, response_set_rules): hetzner_client.request.return_value = response_set_rules actions = bound_firewall.set_rules( [ FirewallRule( direction=FirewallRule.DIRECTION_IN, protocol=FirewallRule.PROTOCOL_ICMP, source_ips=["0.0.0.0/0", "::/0"], description="New firewall description", ) ] ) hetzner_client.request.assert_called_with( url="/firewalls/1/actions/set_rules", method="POST", json={ "rules": [ { "direction": "in", "protocol": "icmp", "source_ips": ["0.0.0.0/0", "::/0"], "description": "New firewall description", } ] }, ) assert actions[0].id == 13 assert actions[0].progress == 100 def test_apply_to_resources( self, hetzner_client, bound_firewall, response_set_rules ): hetzner_client.request.return_value = response_set_rules actions = bound_firewall.apply_to_resources( [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))] ) hetzner_client.request.assert_called_with( url="/firewalls/1/actions/apply_to_resources", method="POST", json={"apply_to": [{"type": "server", "server": {"id": 5}}]}, ) assert actions[0].id == 13 assert actions[0].progress == 100 def test_remove_from_resources( self, hetzner_client, bound_firewall, response_set_rules ): hetzner_client.request.return_value = response_set_rules actions = bound_firewall.remove_from_resources( [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))] ) hetzner_client.request.assert_called_with( url="/firewalls/1/actions/remove_from_resources", method="POST", json={"remove_from": [{"type": "server", "server": {"id": 5}}]}, ) assert actions[0].id == 13 assert actions[0].progress == 100 class TestFirewallsClient(object): @pytest.fixture() def firewalls_client(self): return FirewallsClient(client=mock.MagicMock()) def test_get_by_id(self, firewalls_client, firewall_response): firewalls_client._client.request.return_value = firewall_response firewall = firewalls_client.get_by_id(1) firewalls_client._client.request.assert_called_with( url="/firewalls/1", method="GET" ) assert firewall._client is firewalls_client assert firewall.id == 38 assert firewall.name == "Corporate Intranet Protection" @pytest.mark.parametrize( "params", [ { "name": "Corporate Intranet Protection", "sort": "id", "label_selector": "k==v", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list(self, firewalls_client, two_firewalls_response, params): firewalls_client._client.request.return_value = two_firewalls_response result = firewalls_client.get_list(**params) firewalls_client._client.request.assert_called_with( url="/firewalls", method="GET", params=params ) firewalls = result.firewalls assert result.meta is None assert len(firewalls) == 2 firewalls1 = firewalls[0] firewalls2 = firewalls[1] assert firewalls1._client is firewalls_client assert firewalls1.id == 38 assert firewalls1.name == "Corporate Intranet Protection" assert firewalls2._client is firewalls_client assert firewalls2.id == 39 assert firewalls2.name == "Corporate Extranet Protection" @pytest.mark.parametrize( "params", [ { "name": "Corporate Intranet Protection", "sort": "id", "label_selector": "k==v", }, {}, ], ) def test_get_all(self, firewalls_client, two_firewalls_response, params): firewalls_client._client.request.return_value = two_firewalls_response firewalls = firewalls_client.get_all(**params) params.update({"page": 1, "per_page": 50}) firewalls_client._client.request.assert_called_with( url="/firewalls", method="GET", params=params ) assert len(firewalls) == 2 firewalls1 = firewalls[0] firewalls2 = firewalls[1] assert firewalls1._client is firewalls_client assert firewalls1.id == 38 assert firewalls1.name == "Corporate Intranet Protection" assert firewalls2._client is firewalls_client assert firewalls2.id == 39 assert firewalls2.name == "Corporate Extranet Protection" def test_get_by_name(self, firewalls_client, one_firewalls_response): firewalls_client._client.request.return_value = one_firewalls_response firewall = firewalls_client.get_by_name("Corporate Intranet Protection") params = {"name": "Corporate Intranet Protection"} firewalls_client._client.request.assert_called_with( url="/firewalls", method="GET", params=params ) assert firewall._client is firewalls_client assert firewall.id == 38 assert firewall.name == "Corporate Intranet Protection" @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_get_actions_list(self, firewalls_client, firewall, response_get_actions): firewalls_client._client.request.return_value = response_get_actions result = firewalls_client.get_actions_list(firewall) firewalls_client._client.request.assert_called_with( url="/firewalls/1/actions", method="GET", params={} ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == firewalls_client._client.actions assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" def test_create(self, firewalls_client, response_create_firewall): firewalls_client._client.request.return_value = response_create_firewall response = firewalls_client.create( "Corporate Intranet Protection", rules=[ FirewallRule( direction=FirewallRule.DIRECTION_IN, protocol=FirewallRule.PROTOCOL_ICMP, source_ips=["0.0.0.0/0"], ) ], resources=[ FirewallResource( type=FirewallResource.TYPE_SERVER, server=Server(id=4711) ), FirewallResource( type=FirewallResource.TYPE_LABEL_SELECTOR, label_selector=FirewallResourceLabelSelector(selector="key==value"), ), ], ) firewalls_client._client.request.assert_called_with( url="/firewalls", method="POST", json={ "name": "Corporate Intranet Protection", "rules": [ {"direction": "in", "protocol": "icmp", "source_ips": ["0.0.0.0/0"]} ], "apply_to": [ {"type": "server", "server": {"id": 4711}}, { "type": "label_selector", "label_selector": {"selector": "key==value"}, }, ], }, ) bound_firewall = response.firewall actions = response.actions assert bound_firewall._client is firewalls_client assert bound_firewall.id == 38 assert bound_firewall.name == "Corporate Intranet Protection" assert len(bound_firewall.applied_to) == 2 assert len(actions) == 2 @pytest.mark.parametrize( "firewall", [Firewall(id=38), BoundFirewall(mock.MagicMock(), dict(id=38))] ) def test_update(self, firewalls_client, firewall, response_update_firewall): firewalls_client._client.request.return_value = response_update_firewall firewall = firewalls_client.update( firewall, name="New Corporate Intranet Protection", labels={} ) firewalls_client._client.request.assert_called_with( url="/firewalls/38", method="PUT", json={"name": "New Corporate Intranet Protection", "labels": {}}, ) assert firewall.id == 38 assert firewall.name == "New Corporate Intranet Protection" @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_set_rules(self, firewalls_client, firewall, response_set_rules): firewalls_client._client.request.return_value = response_set_rules actions = firewalls_client.set_rules( firewall, [ FirewallRule( direction=FirewallRule.DIRECTION_IN, protocol=FirewallRule.PROTOCOL_ICMP, source_ips=["0.0.0.0/0", "::/0"], ) ], ) firewalls_client._client.request.assert_called_with( url="/firewalls/1/actions/set_rules", method="POST", json={ "rules": [ { "direction": "in", "protocol": "icmp", "source_ips": ["0.0.0.0/0", "::/0"], } ] }, ) assert actions[0].id == 13 assert actions[0].progress == 100 @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_delete(self, firewalls_client, firewall): delete_success = firewalls_client.delete(firewall) firewalls_client._client.request.assert_called_with( url="/firewalls/1", method="DELETE" ) assert delete_success is True @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_apply_to_resources(self, firewalls_client, firewall, response_set_rules): firewalls_client._client.request.return_value = response_set_rules actions = firewalls_client.apply_to_resources( firewall, [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))], ) firewalls_client._client.request.assert_called_with( url="/firewalls/1/actions/apply_to_resources", method="POST", json={"apply_to": [{"type": "server", "server": {"id": 5}}]}, ) assert actions[0].id == 13 assert actions[0].progress == 100 @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_remove_from_resources( self, firewalls_client, firewall, response_set_rules ): firewalls_client._client.request.return_value = response_set_rules actions = firewalls_client.remove_from_resources( firewall, [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))], ) firewalls_client._client.request.assert_called_with( url="/firewalls/1/actions/remove_from_resources", method="POST", json={"remove_from": [{"type": "server", "server": {"id": 5}}]}, ) assert actions[0].id == 13 assert actions[0].progress == 100 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/firewalls/test_domain.py0000644000175100001710000000054300000000000021410 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.firewalls.domain import Firewall class TestFirewall(object): def test_created_is_datetime(self): firewall = Firewall(id=1, created="2016-01-30T23:50+00:00") assert firewall.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/floating_ips/0000755000175100001710000000000000000000000017214 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/floating_ips/__init__.py0000644000175100001710000000000000000000000021313 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/floating_ips/conftest.py0000644000175100001710000001401400000000000021413 0ustar00runnerdockerimport pytest @pytest.fixture() def floating_ip_response(): return { "floating_ip": { "id": 4711, "description": "Web Frontend", "name": "Web Frontend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, } } @pytest.fixture() def one_floating_ips_response(): return { "floating_ips": [ { "id": 4711, "description": "Web Frontend", "name": "Web Frontend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, }, ] } @pytest.fixture() def two_floating_ips_response(): return { "floating_ips": [ { "id": 4711, "description": "Web Frontend", "name": "Web Frontend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, }, { "id": 4712, "description": "Web Backend", "name": "Web Backend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.2", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, }, ] } @pytest.fixture() def floating_ip_create_response(): return { "floating_ip": { "id": 4711, "description": "Web Frontend", "name": "Web Frontend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, }, "action": { "id": 13, "command": "assign_floating_ip", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, } @pytest.fixture() def response_update_floating_ip(): return { "floating_ip": { "id": 4711, "description": "New description", "name": "New name", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "assign_floating_ip", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/floating_ips/test_client.py0000644000175100001710000003767600000000000022126 0ustar00runnerdockerimport pytest import mock from hcloud.actions.client import BoundAction from hcloud.servers.client import BoundServer from hcloud.servers.domain import Server from hcloud.floating_ips.client import FloatingIPsClient, BoundFloatingIP from hcloud.floating_ips.domain import FloatingIP from hcloud.locations.client import BoundLocation from hcloud.locations.domain import Location class TestBoundFloatingIP(object): @pytest.fixture() def bound_floating_ip(self, hetzner_client): return BoundFloatingIP(client=hetzner_client.floating_ips, data=dict(id=14)) def test_bound_floating_ip_init(self, floating_ip_response): bound_floating_ip = BoundFloatingIP( client=mock.MagicMock(), data=floating_ip_response["floating_ip"] ) assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" assert bound_floating_ip.name == "Web Frontend" assert bound_floating_ip.ip == "131.232.99.1" assert bound_floating_ip.type == "ipv4" assert bound_floating_ip.protection == {"delete": False} assert bound_floating_ip.labels == {} assert bound_floating_ip.blocked is False assert isinstance(bound_floating_ip.server, BoundServer) assert bound_floating_ip.server.id == 42 assert isinstance(bound_floating_ip.home_location, BoundLocation) assert bound_floating_ip.home_location.id == 1 assert bound_floating_ip.home_location.name == "fsn1" assert bound_floating_ip.home_location.description == "Falkenstein DC Park 1" assert bound_floating_ip.home_location.country == "DE" assert bound_floating_ip.home_location.city == "Falkenstein" assert bound_floating_ip.home_location.latitude == 50.47612 assert bound_floating_ip.home_location.longitude == 12.370071 def test_get_actions(self, hetzner_client, bound_floating_ip, response_get_actions): hetzner_client.request.return_value = response_get_actions actions = bound_floating_ip.get_actions(sort="id") hetzner_client.request.assert_called_with( url="/floating_ips/14/actions", method="GET", params={"sort": "id", "page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "assign_floating_ip" def test_update( self, hetzner_client, bound_floating_ip, response_update_floating_ip ): hetzner_client.request.return_value = response_update_floating_ip floating_ip = bound_floating_ip.update( description="New description", name="New name" ) hetzner_client.request.assert_called_with( url="/floating_ips/14", method="PUT", json={"description": "New description", "name": "New name"}, ) assert floating_ip.id == 4711 assert floating_ip.description == "New description" assert floating_ip.name == "New name" def test_delete(self, hetzner_client, bound_floating_ip, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_floating_ip.delete() hetzner_client.request.assert_called_with( url="/floating_ips/14", method="DELETE" ) assert delete_success is True def test_change_protection(self, hetzner_client, bound_floating_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_floating_ip.change_protection(True) hetzner_client.request.assert_called_with( url="/floating_ips/14/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", (Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))) ) def test_assign(self, hetzner_client, bound_floating_ip, server, generic_action): hetzner_client.request.return_value = generic_action action = bound_floating_ip.assign(server) hetzner_client.request.assert_called_with( url="/floating_ips/14/actions/assign", method="POST", json={"server": 1} ) assert action.id == 1 assert action.progress == 0 def test_unassign(self, hetzner_client, bound_floating_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_floating_ip.unassign() hetzner_client.request.assert_called_with( url="/floating_ips/14/actions/unassign", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_change_dns_ptr(self, hetzner_client, bound_floating_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_floating_ip.change_dns_ptr("1.2.3.4", "server02.example.com") hetzner_client.request.assert_called_with( url="/floating_ips/14/actions/change_dns_ptr", method="POST", json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, ) assert action.id == 1 assert action.progress == 0 class TestFloatingIPsClient(object): @pytest.fixture() def floating_ips_client(self): return FloatingIPsClient(client=mock.MagicMock()) def test_get_by_id(self, floating_ips_client, floating_ip_response): floating_ips_client._client.request.return_value = floating_ip_response bound_floating_ip = floating_ips_client.get_by_id(1) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1", method="GET" ) assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" def test_get_by_name(self, floating_ips_client, one_floating_ips_response): floating_ips_client._client.request.return_value = one_floating_ips_response bound_floating_ip = floating_ips_client.get_by_name("Web Frontend") floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="GET", params={"name": "Web Frontend"} ) assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.name == "Web Frontend" assert bound_floating_ip.description == "Web Frontend" @pytest.mark.parametrize( "params", [{"label_selector": "label1", "page": 1, "per_page": 10}, {"name": ""}, {}], ) def test_get_list(self, floating_ips_client, two_floating_ips_response, params): floating_ips_client._client.request.return_value = two_floating_ips_response result = floating_ips_client.get_list(**params) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="GET", params=params ) bound_floating_ips = result.floating_ips assert result.meta is None assert len(bound_floating_ips) == 2 bound_floating_ip1 = bound_floating_ips[0] bound_floating_ip2 = bound_floating_ips[1] assert bound_floating_ip1._client is floating_ips_client assert bound_floating_ip1.id == 4711 assert bound_floating_ip1.description == "Web Frontend" assert bound_floating_ip2._client is floating_ips_client assert bound_floating_ip2.id == 4712 assert bound_floating_ip2.description == "Web Backend" @pytest.mark.parametrize("params", [{"label_selector": "label1"}, {}]) def test_get_all(self, floating_ips_client, two_floating_ips_response, params): floating_ips_client._client.request.return_value = two_floating_ips_response bound_floating_ips = floating_ips_client.get_all(**params) params.update({"page": 1, "per_page": 50}) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="GET", params=params ) assert len(bound_floating_ips) == 2 bound_floating_ip1 = bound_floating_ips[0] bound_floating_ip2 = bound_floating_ips[1] assert bound_floating_ip1._client is floating_ips_client assert bound_floating_ip1.id == 4711 assert bound_floating_ip1.description == "Web Frontend" assert bound_floating_ip2._client is floating_ips_client assert bound_floating_ip2.id == 4712 assert bound_floating_ip2.description == "Web Backend" def test_create_with_location(self, floating_ips_client, floating_ip_response): floating_ips_client._client.request.return_value = floating_ip_response response = floating_ips_client.create( "ipv6", "Web Frontend", home_location=Location(name="location"), ) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="POST", json={ "description": "Web Frontend", "type": "ipv6", "home_location": "location", }, ) bound_floating_ip = response.floating_ip action = response.action assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" assert action is None @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_create_with_server( self, floating_ips_client, server, floating_ip_create_response ): floating_ips_client._client.request.return_value = floating_ip_create_response response = floating_ips_client.create( type="ipv6", description="Web Frontend", server=server ) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="POST", json={"description": "Web Frontend", "type": "ipv6", "server": 1}, ) bound_floating_ip = response.floating_ip action = response.action assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" assert action.id == 13 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_create_with_name( self, floating_ips_client, server, floating_ip_create_response ): floating_ips_client._client.request.return_value = floating_ip_create_response response = floating_ips_client.create( type="ipv6", description="Web Frontend", name="Web Frontend" ) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="POST", json={ "description": "Web Frontend", "type": "ipv6", "name": "Web Frontend", }, ) bound_floating_ip = response.floating_ip action = response.action assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" assert bound_floating_ip.name == "Web Frontend" assert action.id == 13 @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=1), BoundFloatingIP(mock.MagicMock(), dict(id=1))] ) def test_get_actions(self, floating_ips_client, floating_ip, response_get_actions): floating_ips_client._client.request.return_value = response_get_actions actions = floating_ips_client.get_actions(floating_ip) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == floating_ips_client._client.actions assert actions[0].id == 13 assert actions[0].command == "assign_floating_ip" @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=1), BoundFloatingIP(mock.MagicMock(), dict(id=1))] ) def test_update( self, floating_ips_client, floating_ip, response_update_floating_ip ): floating_ips_client._client.request.return_value = response_update_floating_ip floating_ip = floating_ips_client.update( floating_ip, description="New description", name="New name" ) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1", method="PUT", json={"description": "New description", "name": "New name"}, ) assert floating_ip.id == 4711 assert floating_ip.description == "New description" assert floating_ip.name == "New name" @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=1), BoundFloatingIP(mock.MagicMock(), dict(id=1))] ) def test_change_protection(self, floating_ips_client, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action action = floating_ips_client.change_protection(floating_ip, True) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=1), BoundFloatingIP(mock.MagicMock(), dict(id=1))] ) def test_delete(self, floating_ips_client, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action delete_success = floating_ips_client.delete(floating_ip) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1", method="DELETE" ) assert delete_success is True @pytest.mark.parametrize( "server,floating_ip", [ (Server(id=1), FloatingIP(id=12)), ( BoundServer(mock.MagicMock(), dict(id=1)), BoundFloatingIP(mock.MagicMock(), dict(id=12)), ), ], ) def test_assign(self, floating_ips_client, server, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action action = floating_ips_client.assign(floating_ip, server) floating_ips_client._client.request.assert_called_with( url="/floating_ips/12/actions/assign", method="POST", json={"server": 1} ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=12), BoundFloatingIP(mock.MagicMock(), dict(id=12))], ) def test_unassign(self, floating_ips_client, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action action = floating_ips_client.unassign(floating_ip) floating_ips_client._client.request.assert_called_with( url="/floating_ips/12/actions/unassign", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=12), BoundFloatingIP(mock.MagicMock(), dict(id=12))], ) def test_change_dns_ptr(self, floating_ips_client, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action action = floating_ips_client.change_dns_ptr( floating_ip, "1.2.3.4", "server02.example.com" ) floating_ips_client._client.request.assert_called_with( url="/floating_ips/12/actions/change_dns_ptr", method="POST", json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, ) assert action.id == 1 assert action.progress == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/floating_ips/test_domain.py0000644000175100001710000000056000000000000022075 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.floating_ips.domain import FloatingIP class TestFloatingIP(object): def test_created_is_datetime(self): floatingIP = FloatingIP(id=1, created="2016-01-30T23:50+00:00") assert floatingIP.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/helpers/0000755000175100001710000000000000000000000016200 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/helpers/__init__.py0000644000175100001710000000000000000000000020277 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/images/0000755000175100001710000000000000000000000016003 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/images/__init__.py0000644000175100001710000000000000000000000020102 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/images/conftest.py0000644000175100001710000001022100000000000020176 0ustar00runnerdockerimport pytest @pytest.fixture() def image_response(): return { "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": 1, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, } } @pytest.fixture() def two_images_response(): return { "images": [ { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, { "id": 4712, "type": "system", "status": "available", "name": "ubuntu-18.10", "description": "Ubuntu 18.10 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, ] } @pytest.fixture() def one_images_response(): return { "images": [ { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, ] } @pytest.fixture() def response_update_image(): return { "image": { "id": 4711, "type": "snapshot", "status": "available", "name": None, "description": "My new Image description", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "change_protection", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "image"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/images/test_client.py0000644000175100001710000002400600000000000020674 0ustar00runnerdockerimport pytest import mock import datetime from dateutil.tz import tzoffset from hcloud.images.client import ImagesClient, BoundImage from hcloud.actions.client import BoundAction from hcloud.images.domain import Image from hcloud.servers.client import BoundServer class TestBoundImage(object): @pytest.fixture() def bound_image(self, hetzner_client): return BoundImage(client=hetzner_client.images, data=dict(id=14)) def test_bound_image_init(self, image_response): bound_image = BoundImage(client=mock.MagicMock(), data=image_response["image"]) assert bound_image.id == 4711 assert bound_image.type == "snapshot" assert bound_image.status == "available" assert bound_image.name == "ubuntu-20.04" assert bound_image.description == "Ubuntu 20.04 Standard 64 bit" assert bound_image.image_size == 2.3 assert bound_image.disk_size == 10 assert bound_image.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) assert bound_image.os_flavor == "ubuntu" assert bound_image.os_version == "16.04" assert bound_image.rapid_deploy is False assert bound_image.deprecated == datetime.datetime( 2018, 2, 28, 0, 0, tzinfo=tzoffset(None, 0) ) assert isinstance(bound_image.created_from, BoundServer) assert bound_image.created_from.id == 1 assert bound_image.created_from.name == "Server" assert bound_image.created_from.complete is False assert isinstance(bound_image.bound_to, BoundServer) assert bound_image.bound_to.id == 1 assert bound_image.bound_to.complete is False @pytest.mark.parametrize( "params", [{}, {"sort": ["status"], "page": 1, "per_page": 2}] ) def test_get_actions_list( self, hetzner_client, bound_image, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_image.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/images/14/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "change_protection" @pytest.mark.parametrize("params", [{}, {"sort": ["status"]}]) def test_get_actions( self, hetzner_client, bound_image, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions actions = bound_image.get_actions(**params) params.update({"page": 1, "per_page": 50}) hetzner_client.request.assert_called_with( url="/images/14/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_update(self, hetzner_client, bound_image, response_update_image): hetzner_client.request.return_value = response_update_image image = bound_image.update( description="My new Image description", type="snapshot", labels={} ) hetzner_client.request.assert_called_with( url="/images/14", method="PUT", json={ "description": "My new Image description", "type": "snapshot", "labels": {}, }, ) assert image.id == 4711 assert image.description == "My new Image description" def test_delete(self, hetzner_client, bound_image, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_image.delete() hetzner_client.request.assert_called_with(url="/images/14", method="DELETE") assert delete_success is True def test_change_protection(self, hetzner_client, bound_image, generic_action): hetzner_client.request.return_value = generic_action action = bound_image.change_protection(True) hetzner_client.request.assert_called_with( url="/images/14/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 class TestImagesClient(object): @pytest.fixture() def images_client(self): return ImagesClient(client=mock.MagicMock()) def test_get_by_id(self, images_client, image_response): images_client._client.request.return_value = image_response image = images_client.get_by_id(1) images_client._client.request.assert_called_with(url="/images/1", method="GET") assert image._client is images_client assert image.id == 4711 assert image.name == "ubuntu-20.04" @pytest.mark.parametrize( "params", [ { "name": "ubuntu-20.04", "type": "system", "sort": "id", "bound_to": "1", "label_selector": "k==v", "page": 1, "per_page": 10, }, {"name": ""}, {"include_deprecated": True}, {}, ], ) def test_get_list(self, images_client, two_images_response, params): images_client._client.request.return_value = two_images_response result = images_client.get_list(**params) images_client._client.request.assert_called_with( url="/images", method="GET", params=params ) images = result.images assert result.meta is None assert len(images) == 2 images1 = images[0] images2 = images[1] assert images1._client is images_client assert images1.id == 4711 assert images1.name == "ubuntu-20.04" assert images2._client is images_client assert images2.id == 4712 assert images2.name == "ubuntu-18.10" @pytest.mark.parametrize( "params", [ { "name": "ubuntu-20.04", "type": "system", "sort": "id", "bound_to": "1", "label_selector": "k==v", }, {"include_deprecated": True}, {}, ], ) def test_get_all(self, images_client, two_images_response, params): images_client._client.request.return_value = two_images_response images = images_client.get_all(**params) params.update({"page": 1, "per_page": 50}) images_client._client.request.assert_called_with( url="/images", method="GET", params=params ) assert len(images) == 2 images1 = images[0] images2 = images[1] assert images1._client is images_client assert images1.id == 4711 assert images1.name == "ubuntu-20.04" assert images2._client is images_client assert images2.id == 4712 assert images2.name == "ubuntu-18.10" def test_get_by_name(self, images_client, one_images_response): images_client._client.request.return_value = one_images_response image = images_client.get_by_name("ubuntu-20.04") params = {"name": "ubuntu-20.04"} images_client._client.request.assert_called_with( url="/images", method="GET", params=params ) assert image._client is images_client assert image.id == 4711 assert image.name == "ubuntu-20.04" @pytest.mark.parametrize( "image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))] ) def test_get_actions_list(self, images_client, image, response_get_actions): images_client._client.request.return_value = response_get_actions result = images_client.get_actions_list(image) images_client._client.request.assert_called_with( url="/images/1/actions", method="GET", params={} ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == images_client._client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @pytest.mark.parametrize( "image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))] ) def test_update(self, images_client, image, response_update_image): images_client._client.request.return_value = response_update_image image = images_client.update( image, description="My new Image description", type="snapshot", labels={} ) images_client._client.request.assert_called_with( url="/images/1", method="PUT", json={ "description": "My new Image description", "type": "snapshot", "labels": {}, }, ) assert image.id == 4711 assert image.description == "My new Image description" @pytest.mark.parametrize( "image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))] ) def test_change_protection(self, images_client, image, generic_action): images_client._client.request.return_value = generic_action action = images_client.change_protection(image, True) images_client._client.request.assert_called_with( url="/images/1/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))] ) def test_delete(self, images_client, image, generic_action): images_client._client.request.return_value = generic_action delete_success = images_client.delete(image) images_client._client.request.assert_called_with( url="/images/1", method="DELETE" ) assert delete_success is True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/images/test_domain.py0000644000175100001710000000052100000000000020661 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.images.domain import Image class TestImage(object): def test_created_is_datetime(self): image = Image(id=1, created="2016-01-30T23:50+00:00") assert image.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/isos/0000755000175100001710000000000000000000000015513 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/isos/__init__.py0000644000175100001710000000000000000000000017612 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/isos/conftest.py0000644000175100001710000000237200000000000017716 0ustar00runnerdockerimport pytest @pytest.fixture() def iso_response(): return { "iso": { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00", } } @pytest.fixture() def two_isos_response(): return { "isos": [ { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00", }, { "id": 4712, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00", }, ] } @pytest.fixture() def one_isos_response(): return { "isos": [ { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00", } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/isos/test_client.py0000644000175100001710000000671300000000000020411 0ustar00runnerdockerimport pytest import mock import datetime from dateutil.tz import tzoffset from hcloud.isos.client import IsosClient, BoundIso class TestBoundIso(object): @pytest.fixture() def bound_iso(self, hetzner_client): return BoundIso(client=hetzner_client.isos, data=dict(id=14)) def test_bound_iso_init(self, iso_response): bound_iso = BoundIso(client=mock.MagicMock(), data=iso_response["iso"]) assert bound_iso.id == 4711 assert bound_iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" assert bound_iso.description == "FreeBSD 11.0 x64" assert bound_iso.type == "public" assert bound_iso.deprecated == datetime.datetime( 2018, 2, 28, 0, 0, tzinfo=tzoffset(None, 0) ) class TestIsosClient(object): @pytest.fixture() def isos_client(self): return IsosClient(client=mock.MagicMock()) def test_get_by_id(self, isos_client, iso_response): isos_client._client.request.return_value = iso_response iso = isos_client.get_by_id(1) isos_client._client.request.assert_called_with(url="/isos/1", method="GET") assert iso._client is isos_client assert iso.id == 4711 assert iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" @pytest.mark.parametrize( "params", [ {}, {"name": ""}, {"name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "page": 1, "per_page": 2}, ], ) def test_get_list(self, isos_client, two_isos_response, params): isos_client._client.request.return_value = two_isos_response result = isos_client.get_list(**params) isos_client._client.request.assert_called_with( url="/isos", method="GET", params=params ) isos = result.isos assert result.meta is None assert len(isos) == 2 isos1 = isos[0] isos2 = isos[1] assert isos1._client is isos_client assert isos1.id == 4711 assert isos1.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" assert isos2._client is isos_client assert isos2.id == 4712 assert isos2.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" @pytest.mark.parametrize( "params", [{}, {"name": "FreeBSD-11.0-RELEASE-amd64-dvd1"}] ) def test_get_all(self, isos_client, two_isos_response, params): isos_client._client.request.return_value = two_isos_response isos = isos_client.get_all(**params) params.update({"page": 1, "per_page": 50}) isos_client._client.request.assert_called_with( url="/isos", method="GET", params=params ) assert len(isos) == 2 isos1 = isos[0] isos2 = isos[1] assert isos1._client is isos_client assert isos1.id == 4711 assert isos1.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" assert isos2._client is isos_client assert isos2.id == 4712 assert isos2.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" def test_get_by_name(self, isos_client, one_isos_response): isos_client._client.request.return_value = one_isos_response iso = isos_client.get_by_name("FreeBSD-11.0-RELEASE-amd64-dvd1") params = {"name": "FreeBSD-11.0-RELEASE-amd64-dvd1"} isos_client._client.request.assert_called_with( url="/isos", method="GET", params=params ) assert iso._client is isos_client assert iso.id == 4711 assert iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/isos/test_domain.py0000644000175100001710000000051600000000000020375 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.isos.domain import Iso class TestIso(object): def test_deprecated_is_datetime(self): iso = Iso(id=1, deprecated="2016-01-30T23:50+00:00") assert iso.deprecated == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/load_balancer_types/0000755000175100001710000000000000000000000020530 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/load_balancer_types/__init__.py0000644000175100001710000000000000000000000022627 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/load_balancer_types/conftest.py0000644000175100001710000000665700000000000022745 0ustar00runnerdockerimport pytest @pytest.fixture() def load_balancer_type_response(): return { "load_balancer_type": { "id": 1, "name": "LB11", "description": "LB11", "max_connections": 1, "max_services": 1, "max_targets": 1, "max_assigned_certificates": 1, "deprecated": None, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], } } @pytest.fixture() def two_load_balancer_types_response(): return { "load_balancer_types": [ { "id": 1, "name": "LB11", "description": "LB11D", "max_connections": 1, "max_services": 1, "max_targets": 1, "max_assigned_certificates": 1, "deprecated": None, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, { "id": 2, "name": "LB21", "description": "LB21D", "max_connections": 2, "max_services": 2, "max_targets": 2, "max_assigned_certificates": 2, "deprecated": None, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, ] } @pytest.fixture() def one_load_balancer_types_response(): return { "load_balancer_types": [ { "id": 2, "name": "LB21", "description": "LB21D", "max_connections": 2, "max_services": 2, "max_targets": 2, "max_assigned_certificates": 2, "deprecated": None, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/load_balancer_types/test_client.py0000644000175100001710000000721400000000000023423 0ustar00runnerdockerimport pytest import mock from hcloud.load_balancer_types.client import LoadBalancerTypesClient class TestLoadBalancerTypesClient(object): @pytest.fixture() def load_balancer_types_client(self): return LoadBalancerTypesClient(client=mock.MagicMock()) def test_get_by_id(self, load_balancer_types_client, load_balancer_type_response): load_balancer_types_client._client.request.return_value = ( load_balancer_type_response ) load_balancer_type = load_balancer_types_client.get_by_id(1) load_balancer_types_client._client.request.assert_called_with( url="/load_balancer_types/1", method="GET" ) assert load_balancer_type._client is load_balancer_types_client assert load_balancer_type.id == 1 assert load_balancer_type.name == "LB11" @pytest.mark.parametrize( "params", [{"name": "LB11", "page": 1, "per_page": 10}, {"name": ""}, {}] ) def test_get_list( self, load_balancer_types_client, two_load_balancer_types_response, params ): load_balancer_types_client._client.request.return_value = ( two_load_balancer_types_response ) result = load_balancer_types_client.get_list(**params) load_balancer_types_client._client.request.assert_called_with( url="/load_balancer_types", method="GET", params=params ) load_balancer_types = result.load_balancer_types assert result.meta is None assert len(load_balancer_types) == 2 load_balancer_types1 = load_balancer_types[0] load_balancer_types2 = load_balancer_types[1] assert load_balancer_types1._client is load_balancer_types_client assert load_balancer_types1.id == 1 assert load_balancer_types1.name == "LB11" assert load_balancer_types2._client is load_balancer_types_client assert load_balancer_types2.id == 2 assert load_balancer_types2.name == "LB21" @pytest.mark.parametrize("params", [{"name": "LB21"}]) def test_get_all( self, load_balancer_types_client, two_load_balancer_types_response, params ): load_balancer_types_client._client.request.return_value = ( two_load_balancer_types_response ) load_balancer_types = load_balancer_types_client.get_all(**params) params.update({"page": 1, "per_page": 50}) load_balancer_types_client._client.request.assert_called_with( url="/load_balancer_types", method="GET", params=params ) assert len(load_balancer_types) == 2 load_balancer_types1 = load_balancer_types[0] load_balancer_types2 = load_balancer_types[1] assert load_balancer_types1._client is load_balancer_types_client assert load_balancer_types1.id == 1 assert load_balancer_types1.name == "LB11" assert load_balancer_types2._client is load_balancer_types_client assert load_balancer_types2.id == 2 assert load_balancer_types2.name == "LB21" def test_get_by_name( self, load_balancer_types_client, one_load_balancer_types_response ): load_balancer_types_client._client.request.return_value = ( one_load_balancer_types_response ) load_balancer_type = load_balancer_types_client.get_by_name("LB21") params = {"name": "LB21"} load_balancer_types_client._client.request.assert_called_with( url="/load_balancer_types", method="GET", params=params ) assert load_balancer_type._client is load_balancer_types_client assert load_balancer_type.id == 2 assert load_balancer_type.name == "LB21" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/load_balancers/0000755000175100001710000000000000000000000017467 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/load_balancers/__init__.py0000644000175100001710000000000000000000000021566 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/load_balancers/conftest.py0000644000175100001710000005411500000000000021674 0ustar00runnerdockerimport pytest @pytest.fixture() def response_load_balancer(): return { "load_balancer": { "id": 4711, "name": "Web Frontend", "ipv4": "131.232.99.1", "ipv6": "2001:db8::1", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "protection": {"delete": False}, "labels": {}, "created": "2016-01-30T23:50:00+00:00", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, "sticky_sessions": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "health_status": [{"listen_port": 443, "status": "healthy"}], "label_selector": None, "use_private_ip": False, } ], "algorithm": {"type": "round_robin"}, } } @pytest.fixture() def response_create_load_balancer(): return { "load_balancer": { "id": 1, "name": "my-balancer", "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "network_zone": "eu-central", "algorithm": {"type": "round_robin"}, "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, "sticky_sessions": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "label_selector": None, "use_private_ip": False, } ], }, "action": { "id": 1, "command": "create_load_balancer", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, } @pytest.fixture() def response_update_load_balancer(): return { "load_balancer": { "id": 4711, "name": "new-name", "ipv4": "131.232.99.1", "ipv6": "2001:db8::1", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "protection": {"delete": False}, "labels": {"labelkey": "value"}, "created": "2016-01-30T23:50:00+00:00", "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, "sticky_sessions": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "use_private_ip": False, "health_status": [{"listen_port": 443, "status": "healthy"}], "label_selector": None, } ], "algorithm": {"type": "round_robin"}, } } @pytest.fixture() def response_simple_load_balancers(): return { "load_balancers": [ { "id": 4711, "name": "Web Frontend", "ipv4": "131.232.99.1", "ipv6": "2001:db8::1", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "protection": {"delete": False}, "labels": {}, "created": "2016-01-30T23:50:00+00:00", "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "sticky_sessions": True, "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "use_private_ip": False, "health_status": [{"listen_port": 443, "status": "healthy"}], "label_selector": None, } ], "algorithm": {"type": "round_robin"}, }, { "id": 4712, "name": "Web Frontend2", "ipv4": "131.232.99.1", "ipv6": "2001:db8::1", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "protection": {"delete": False}, "labels": {}, "created": "2016-01-30T23:50:00+00:00", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "sticky_sessions": True, "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "health_status": [{"listen_port": 443, "status": "healthy"}], "label_selector": None, "use_private_ip": False, } ], "algorithm": {"type": "round_robin"}, }, ] } @pytest.fixture() def response_add_service(): return { "action": { "id": 13, "command": "add_service", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_delete_service(): return { "action": { "id": 13, "command": "delete_service", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_add_target(): return { "action": { "id": 13, "command": "add_target", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_remove_target(): return { "action": { "id": 13, "command": "remove_target", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_update_service(): return { "action": { "id": 13, "command": "update_service", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_change_algorithm(): return { "action": { "id": 13, "command": "change_algorithm", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_change_reverse_dns_entry(): return { "action": { "id": 13, "command": "change_dns_ptr", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_change_protection(): return { "action": { "id": 13, "command": "change_protection", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_enable_public_interface(): return { "action": { "id": 13, "command": "enable_public_interface", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_disable_public_interface(): return { "action": { "id": 13, "command": "disable_public_interface", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_attach_load_balancer_to_network(): return { "action": { "id": 13, "command": "attach_to_network", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_detach_from_network(): return { "action": { "id": 13, "command": "detach_from_network", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "change_protection", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 14, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/load_balancers/test_client.py0000644000175100001710000004522300000000000022364 0ustar00runnerdockerimport mock import pytest from hcloud.load_balancer_types.domain import LoadBalancerType from hcloud.locations.domain import Location from hcloud.networks.domain import Network from hcloud.servers.domain import Server from hcloud.load_balancers.client import BoundLoadBalancer, LoadBalancersClient from hcloud.load_balancers.domain import ( LoadBalancerAlgorithm, LoadBalancerHealthCheck, LoadBalancerService, LoadBalancerTarget, LoadBalancer, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, ) from hcloud.actions.client import BoundAction class TestBoundLoadBalancer(object): @pytest.fixture() def bound_load_balancer(self, hetzner_client): return BoundLoadBalancer(client=hetzner_client.load_balancers, data=dict(id=14)) def test_bound_load_balancer_init(self, response_load_balancer): bound_load_balancer = BoundLoadBalancer( client=mock.MagicMock(), data=response_load_balancer["load_balancer"] ) assert bound_load_balancer.id == 4711 assert bound_load_balancer.name == "Web Frontend" @pytest.mark.parametrize("params", [{"page": 1, "per_page": 10}, {}]) def test_get_actions_list( self, hetzner_client, bound_load_balancer, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_load_balancer.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/load_balancers/14/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "change_protection" @pytest.mark.parametrize("params", [{}]) def test_get_actions( self, hetzner_client, bound_load_balancer, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions actions = bound_load_balancer.get_actions(**params) params.update({"page": 1, "per_page": 50}) hetzner_client.request.assert_called_with( url="/load_balancers/14/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_update( self, hetzner_client, bound_load_balancer, response_update_load_balancer ): hetzner_client.request.return_value = response_update_load_balancer load_balancer = bound_load_balancer.update(name="new-name", labels={}) hetzner_client.request.assert_called_with( url="/load_balancers/14", method="PUT", json={"name": "new-name", "labels": {}}, ) assert load_balancer.id == 4711 assert load_balancer.name == "new-name" def test_delete(self, hetzner_client, generic_action, bound_load_balancer): hetzner_client.request.return_value = generic_action delete_success = bound_load_balancer.delete() hetzner_client.request.assert_called_with( url="/load_balancers/14", method="DELETE" ) assert delete_success is True def test_add_service( self, hetzner_client, response_add_service, bound_load_balancer ): hetzner_client.request.return_value = response_add_service service = LoadBalancerService(listen_port=80, protocol="http") action = bound_load_balancer.add_service(service) hetzner_client.request.assert_called_with( json={"protocol": "http", "listen_port": 80}, url="/load_balancers/14/actions/add_service", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "add_service" def test_delete_service( self, hetzner_client, response_delete_service, bound_load_balancer ): hetzner_client.request.return_value = response_delete_service service = LoadBalancerService(listen_port=12) action = bound_load_balancer.delete_service(service) hetzner_client.request.assert_called_with( json={"listen_port": 12}, url="/load_balancers/14/actions/delete_service", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "delete_service" @pytest.mark.parametrize( "target,params", [ ( LoadBalancerTarget( type="server", server=Server(id=1), use_private_ip=True ), {"server": {"id": 1}}, ), ( LoadBalancerTarget(type="ip", ip=LoadBalancerTargetIP(ip="127.0.0.1")), {"ip": {"ip": "127.0.0.1"}}, ), ( LoadBalancerTarget( type="label_selector", label_selector=LoadBalancerTargetLabelSelector(selector="abc=def"), ), {"label_selector": {"selector": "abc=def"}}, ), ], ) def test_add_target( self, hetzner_client, response_add_target, bound_load_balancer, target, params ): hetzner_client.request.return_value = response_add_target action = bound_load_balancer.add_target(target) params.update({"type": target.type, "use_private_ip": target.use_private_ip}) hetzner_client.request.assert_called_with( json=params, url="/load_balancers/14/actions/add_target", method="POST" ) assert action.id == 13 assert action.progress == 100 assert action.command == "add_target" @pytest.mark.parametrize( "target,params", [ ( LoadBalancerTarget( type="server", server=Server(id=1), use_private_ip=True ), {"server": {"id": 1}}, ), ( LoadBalancerTarget(type="ip", ip=LoadBalancerTargetIP(ip="127.0.0.1")), {"ip": {"ip": "127.0.0.1"}}, ), ( LoadBalancerTarget( type="label_selector", label_selector=LoadBalancerTargetLabelSelector(selector="abc=def"), ), {"label_selector": {"selector": "abc=def"}}, ), ], ) def test_remove_target( self, hetzner_client, response_remove_target, bound_load_balancer, target, params, ): hetzner_client.request.return_value = response_remove_target action = bound_load_balancer.remove_target(target) params.update({"type": target.type}) hetzner_client.request.assert_called_with( json=params, url="/load_balancers/14/actions/remove_target", method="POST" ) assert action.id == 13 assert action.progress == 100 assert action.command == "remove_target" def test_update_service( self, hetzner_client, response_update_service, bound_load_balancer ): hetzner_client.request.return_value = response_update_service new_health_check = LoadBalancerHealthCheck( protocol="http", port=13, interval=1, timeout=1, retries=1 ) service = LoadBalancerService(listen_port=12, health_check=new_health_check) action = bound_load_balancer.update_service(service) hetzner_client.request.assert_called_with( json={ "listen_port": 12, "health_check": { "protocol": "http", "port": 13, "interval": 1, "timeout": 1, "retries": 1, }, }, url="/load_balancers/14/actions/update_service", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "update_service" def test_change_algorithm( self, hetzner_client, response_change_algorithm, bound_load_balancer ): hetzner_client.request.return_value = response_change_algorithm algorithm = LoadBalancerAlgorithm(type="round_robin") action = bound_load_balancer.change_algorithm(algorithm) hetzner_client.request.assert_called_with( json={"type": "round_robin"}, url="/load_balancers/14/actions/change_algorithm", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "change_algorithm" def test_change_dns_ptr( self, hetzner_client, response_change_reverse_dns_entry, bound_load_balancer ): hetzner_client.request.return_value = response_change_reverse_dns_entry action = bound_load_balancer.change_dns_ptr( ip="1.2.3.4", dns_ptr="lb1.example.com" ) hetzner_client.request.assert_called_with( json={"dns_ptr": "lb1.example.com", "ip": "1.2.3.4"}, url="/load_balancers/14/actions/change_dns_ptr", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "change_dns_ptr" def test_change_protection( self, hetzner_client, response_change_protection, bound_load_balancer ): hetzner_client.request.return_value = response_change_protection action = bound_load_balancer.change_protection(delete=True) hetzner_client.request.assert_called_with( json={"delete": True}, url="/load_balancers/14/actions/change_protection", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "change_protection" def test_enable_public_interface( self, response_enable_public_interface, hetzner_client, bound_load_balancer ): hetzner_client.request.return_value = response_enable_public_interface action = bound_load_balancer.enable_public_interface() hetzner_client.request.assert_called_with( url="/load_balancers/14/actions/enable_public_interface", method="POST" ) assert action.id == 13 assert action.progress == 100 assert action.command == "enable_public_interface" def test_disable_public_interface( self, response_disable_public_interface, hetzner_client, bound_load_balancer ): hetzner_client.request.return_value = response_disable_public_interface action = bound_load_balancer.disable_public_interface() hetzner_client.request.assert_called_with( url="/load_balancers/14/actions/disable_public_interface", method="POST" ) assert action.id == 13 assert action.progress == 100 assert action.command == "disable_public_interface" def test_attach_to_network( self, response_attach_load_balancer_to_network, hetzner_client, bound_load_balancer, ): hetzner_client.request.return_value = response_attach_load_balancer_to_network action = bound_load_balancer.attach_to_network(Network(id=1)) hetzner_client.request.assert_called_with( json={"network": 1}, url="/load_balancers/14/actions/attach_to_network", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "attach_to_network" def test_detach_from_network( self, response_detach_from_network, hetzner_client, bound_load_balancer ): hetzner_client.request.return_value = response_detach_from_network action = bound_load_balancer.detach_from_network(Network(id=1)) hetzner_client.request.assert_called_with( json={"network": 1}, url="/load_balancers/14/actions/detach_from_network", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "detach_from_network" def test_change_type(self, hetzner_client, bound_load_balancer, generic_action): hetzner_client.request.return_value = generic_action action = bound_load_balancer.change_type(LoadBalancerType(name="lb21")) hetzner_client.request.assert_called_with( url="/load_balancers/14/actions/change_type", method="POST", json={"load_balancer_type": "lb21"}, ) assert action.id == 1 assert action.progress == 0 class TestLoadBalancerslient(object): @pytest.fixture() def load_balancers_client(self): return LoadBalancersClient(client=mock.MagicMock()) def test_get_by_id(self, load_balancers_client, response_load_balancer): load_balancers_client._client.request.return_value = response_load_balancer bound_load_balancer = load_balancers_client.get_by_id(1) load_balancers_client._client.request.assert_called_with( url="/load_balancers/1", method="GET" ) assert bound_load_balancer._client is load_balancers_client assert bound_load_balancer.id == 4711 assert bound_load_balancer.name == "Web Frontend" assert bound_load_balancer.outgoing_traffic == 123456 assert bound_load_balancer.ingoing_traffic == 123456 assert bound_load_balancer.included_traffic == 654321 @pytest.mark.parametrize( "params", [ { "name": "load_balancer1", "label_selector": "label1", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list( self, load_balancers_client, response_simple_load_balancers, params ): load_balancers_client._client.request.return_value = ( response_simple_load_balancers ) result = load_balancers_client.get_list(**params) load_balancers_client._client.request.assert_called_with( url="/load_balancers", method="GET", params=params ) bound_load_balancers = result.load_balancers assert result.meta is None assert len(bound_load_balancers) == 2 bound_load_balancer1 = bound_load_balancers[0] bound_load_balancer2 = bound_load_balancers[1] assert bound_load_balancer1._client is load_balancers_client assert bound_load_balancer1.id == 4711 assert bound_load_balancer1.name == "Web Frontend" assert bound_load_balancer2._client is load_balancers_client assert bound_load_balancer2.id == 4712 assert bound_load_balancer2.name == "Web Frontend2" @pytest.mark.parametrize( "params", [{"name": "loadbalancer1", "label_selector": "label1"}, {}] ) def test_get_all( self, load_balancers_client, response_simple_load_balancers, params ): load_balancers_client._client.request.return_value = ( response_simple_load_balancers ) bound_load_balancers = load_balancers_client.get_all(**params) params.update({"page": 1, "per_page": 50}) load_balancers_client._client.request.assert_called_with( url="/load_balancers", method="GET", params=params ) assert len(bound_load_balancers) == 2 bound_load_balancer1 = bound_load_balancers[0] bound_load_balancer2 = bound_load_balancers[1] assert bound_load_balancer1._client is load_balancers_client assert bound_load_balancer1.id == 4711 assert bound_load_balancer1.name == "Web Frontend" assert bound_load_balancer2._client is load_balancers_client assert bound_load_balancer2.id == 4712 assert bound_load_balancer2.name == "Web Frontend2" def test_get_by_name(self, load_balancers_client, response_simple_load_balancers): load_balancers_client._client.request.return_value = ( response_simple_load_balancers ) bound_load_balancer = load_balancers_client.get_by_name("Web Frontend") params = {"name": "Web Frontend"} load_balancers_client._client.request.assert_called_with( url="/load_balancers", method="GET", params=params ) assert bound_load_balancer._client is load_balancers_client assert bound_load_balancer.id == 4711 assert bound_load_balancer.name == "Web Frontend" def test_create(self, load_balancers_client, response_create_load_balancer): load_balancers_client._client.request.return_value = ( response_create_load_balancer ) response = load_balancers_client.create( "my-balancer", load_balancer_type=LoadBalancerType(name="lb11"), location=Location(id=1), ) load_balancers_client._client.request.assert_called_with( url="/load_balancers", method="POST", json={"name": "my-balancer", "load_balancer_type": "lb11", "location": 1}, ) bound_load_balancer = response.load_balancer assert bound_load_balancer._client is load_balancers_client assert bound_load_balancer.id == 1 assert bound_load_balancer.name == "my-balancer" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_change_type_with_load_balancer_type_name( self, load_balancers_client, load_balancer, generic_action ): load_balancers_client._client.request.return_value = generic_action action = load_balancers_client.change_type( load_balancer, LoadBalancerType(name="lb11") ) load_balancers_client._client.request.assert_called_with( url="/load_balancers/1/actions/change_type", method="POST", json={"load_balancer_type": "lb11"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_change_type_with_load_balancer_type_id( self, load_balancers_client, load_balancer, generic_action ): load_balancers_client._client.request.return_value = generic_action action = load_balancers_client.change_type( load_balancer, LoadBalancerType(id=1) ) load_balancers_client._client.request.assert_called_with( url="/load_balancers/1/actions/change_type", method="POST", json={"load_balancer_type": 1}, ) assert action.id == 1 assert action.progress == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/load_balancers/test_domain.py0000644000175100001710000000055100000000000022350 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.load_balancers.domain import LoadBalancer class TestLoadBalancers(object): def test_created_is_datetime(self): lb = LoadBalancer(id=1, created="2016-01-30T23:50+00:00") assert lb.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/locations/0000755000175100001710000000000000000000000016531 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/locations/__init__.py0000644000175100001710000000000000000000000020630 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/locations/conftest.py0000644000175100001710000000310100000000000020723 0ustar00runnerdockerimport pytest @pytest.fixture() def location_response(): return { "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", } } @pytest.fixture() def two_locations_response(): return { "locations": [ { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, { "id": 2, "name": "nbg1", "description": "Nuremberg DC Park 1", "country": "DE", "city": "Nuremberg", "latitude": 49.452102, "longitude": 11.076665, "network_zone": "eu-central", }, ] } @pytest.fixture() def one_locations_response(): return { "locations": [ { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/locations/test_client.py0000644000175100001710000000621100000000000021420 0ustar00runnerdockerimport pytest # noqa: F401 import mock # noqa: F401 from hcloud.locations.client import LocationsClient class TestLocationsClient(object): @pytest.fixture() def locations_client(self): return LocationsClient(client=mock.MagicMock()) def test_get_by_id(self, locations_client, location_response): locations_client._client.request.return_value = location_response location = locations_client.get_by_id(1) locations_client._client.request.assert_called_with( url="/locations/1", method="GET" ) assert location._client is locations_client assert location.id == 1 assert location.name == "fsn1" assert location.network_zone == "eu-central" @pytest.mark.parametrize( "params", [{"name": "fsn1", "page": 1, "per_page": 10}, {"name": ""}, {}] ) def test_get_list(self, locations_client, two_locations_response, params): locations_client._client.request.return_value = two_locations_response result = locations_client.get_list(**params) locations_client._client.request.assert_called_with( url="/locations", method="GET", params=params ) locations = result.locations assert result.meta is None assert len(locations) == 2 location1 = locations[0] location2 = locations[1] assert location1._client is locations_client assert location1.id == 1 assert location1.name == "fsn1" assert location1.network_zone == "eu-central" assert location2._client is locations_client assert location2.id == 2 assert location2.name == "nbg1" assert location2.network_zone == "eu-central" @pytest.mark.parametrize("params", [{"name": "fsn1"}, {}]) def test_get_all(self, locations_client, two_locations_response, params): locations_client._client.request.return_value = two_locations_response locations = locations_client.get_all(**params) params.update({"page": 1, "per_page": 50}) locations_client._client.request.assert_called_with( url="/locations", method="GET", params=params ) assert len(locations) == 2 location1 = locations[0] location2 = locations[1] assert location1._client is locations_client assert location1.id == 1 assert location1.name == "fsn1" assert location1.network_zone == "eu-central" assert location2._client is locations_client assert location2.id == 2 assert location2.name == "nbg1" assert location2.network_zone == "eu-central" def test_get_by_name(self, locations_client, one_locations_response): locations_client._client.request.return_value = one_locations_response location = locations_client.get_by_name("fsn1") params = {"name": "fsn1"} locations_client._client.request.assert_called_with( url="/locations", method="GET", params=params ) assert location._client is locations_client assert location.id == 1 assert location.name == "fsn1" assert location.network_zone == "eu-central" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8505113 hcloud-1.16.0/tests/unit/networks/0000755000175100001710000000000000000000000016412 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/networks/__init__.py0000644000175100001710000000000000000000000020511 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/networks/conftest.py0000644000175100001710000001253700000000000020621 0ustar00runnerdockerimport pytest @pytest.fixture() def network_response(): return { "network": { "id": 1, "name": "mynet", "created": "2016-01-30T23:50:11+00:00", "ip_range": "10.0.0.0/16", "subnets": [ { "type": "cloud", "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "gateway": "10.0.0.1", }, { "type": "vswitch", "ip_range": "10.0.3.0/24", "network_zone": "eu-central", "gateway": "10.0.3.1", }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], "servers": [42], "protection": {"delete": False}, "labels": {}, } } @pytest.fixture() def two_networks_response(): return { "networks": [ { "id": 1, "name": "mynet", "created": "2016-01-30T23:50:11+00:00", "ip_range": "10.0.0.0/16", "subnets": [ { "type": "cloud", "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "gateway": "10.0.0.1", }, { "type": "vswitch", "ip_range": "10.0.3.0/24", "network_zone": "eu-central", "gateway": "10.0.3.1", }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], "servers": [42], "protection": {"delete": False}, "labels": {}, }, { "id": 2, "name": "myanothernet", "created": "2016-01-30T23:50:11+00:00", "ip_range": "12.0.0.0/8", "subnets": [ { "type": "cloud", "ip_range": "12.0.1.0/24", "network_zone": "eu-central", "gateway": "12.0.0.1", } ], "routes": [{"destination": "12.100.1.0/24", "gateway": "12.0.1.1"}], "servers": [45], "protection": {"delete": False}, "labels": {}, }, ] } @pytest.fixture() def one_network_response(): return { "networks": [ { "id": 1, "name": "mynet", "created": "2016-01-30T23:50:11+00:00", "ip_range": "10.0.0.0/16", "subnets": [ { "type": "cloud", "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "gateway": "10.0.0.1", }, { "type": "vswitch", "ip_range": "10.0.3.0/24", "network_zone": "eu-central", "gateway": "10.0.3.1", }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], "servers": [42], "protection": {"delete": False}, "labels": {}, } ] } @pytest.fixture() def network_create_response(): return { "network": { "id": 4711, "name": "mynet", "ip_range": "10.0.0.0/16", "subnets": [ { "type": "cloud", "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "gateway": "10.0.0.1", } ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], "servers": [42], "protection": {"delete": False}, "labels": {}, "created": "2016-01-30T23:50:00+00:00", } } @pytest.fixture() def response_update_network(): return { "network": { "id": 4711, "name": "new-name", "ip_range": "10.0.0.0/16", "subnets": [ { "type": "cloud", "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "gateway": "10.0.0.1", } ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], "servers": [42], "protection": {"delete": False}, "labels": {}, "created": "2016-01-30T23:50:00+00:00", } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "add_subnet", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "network"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/networks/test_client.py0000644000175100001710000004560400000000000021312 0ustar00runnerdockerimport pytest from dateutil.parser import isoparse import mock from hcloud.actions.client import BoundAction from hcloud.networks.client import BoundNetwork, NetworksClient from hcloud.networks.domain import NetworkSubnet, NetworkRoute, Network from hcloud.servers.client import BoundServer class TestBoundNetwork(object): @pytest.fixture() def bound_network(self, hetzner_client): return BoundNetwork(client=hetzner_client.networks, data=dict(id=14)) def test_bound_network_init(self, network_response): bound_network = BoundNetwork( client=mock.MagicMock(), data=network_response["network"] ) assert bound_network.id == 1 assert bound_network.created == isoparse("2016-01-30T23:50:11+00:00") assert bound_network.name == "mynet" assert bound_network.ip_range == "10.0.0.0/16" assert bound_network.protection["delete"] is False assert len(bound_network.servers) == 1 assert isinstance(bound_network.servers[0], BoundServer) assert bound_network.servers[0].id == 42 assert bound_network.servers[0].complete is False assert len(bound_network.subnets) == 2 assert isinstance(bound_network.subnets[0], NetworkSubnet) assert bound_network.subnets[0].type == NetworkSubnet.TYPE_CLOUD assert bound_network.subnets[0].ip_range == "10.0.1.0/24" assert bound_network.subnets[0].network_zone == "eu-central" assert bound_network.subnets[0].gateway == "10.0.0.1" assert len(bound_network.routes) == 1 assert isinstance(bound_network.routes[0], NetworkRoute) assert bound_network.routes[0].destination == "10.100.1.0/24" assert bound_network.routes[0].gateway == "10.0.1.1" def test_get_actions(self, hetzner_client, bound_network, response_get_actions): hetzner_client.request.return_value = response_get_actions actions = bound_network.get_actions(sort="id") hetzner_client.request.assert_called_with( url="/networks/14/actions", method="GET", params={"page": 1, "per_page": 50, "sort": "id"}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "add_subnet" def test_update(self, hetzner_client, bound_network, response_update_network): hetzner_client.request.return_value = response_update_network network = bound_network.update(name="new-name") hetzner_client.request.assert_called_with( url="/networks/14", method="PUT", json={"name": "new-name"} ) assert network.id == 4711 assert network.name == "new-name" def test_delete(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_network.delete() hetzner_client.request.assert_called_with(url="/networks/14", method="DELETE") assert delete_success is True def test_change_protection(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action action = bound_network.change_protection(True) hetzner_client.request.assert_called_with( url="/networks/14/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 def test_add_subnet(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action subnet = NetworkSubnet( type=NetworkSubnet.TYPE_CLOUD, ip_range="10.0.1.0/24", network_zone="eu-central", ) action = bound_network.add_subnet(subnet) hetzner_client.request.assert_called_with( url="/networks/14/actions/add_subnet", method="POST", json={ "type": NetworkSubnet.TYPE_CLOUD, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", }, ) assert action.id == 1 assert action.progress == 0 def test_delete_subnet(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action subnet = NetworkSubnet(ip_range="10.0.1.0/24") action = bound_network.delete_subnet(subnet) hetzner_client.request.assert_called_with( url="/networks/14/actions/delete_subnet", method="POST", json={"ip_range": "10.0.1.0/24"}, ) assert action.id == 1 assert action.progress == 0 def test_add_route(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action route = NetworkRoute(destination="10.100.1.0/24", gateway="10.0.1.1") action = bound_network.add_route(route) hetzner_client.request.assert_called_with( url="/networks/14/actions/add_route", method="POST", json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, ) assert action.id == 1 assert action.progress == 0 def test_delete_route(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action route = NetworkRoute(destination="10.100.1.0/24", gateway="10.0.1.1") action = bound_network.delete_route(route) hetzner_client.request.assert_called_with( url="/networks/14/actions/delete_route", method="POST", json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, ) assert action.id == 1 assert action.progress == 0 def test_change_ip(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action action = bound_network.change_ip_range("10.0.0.0/12") hetzner_client.request.assert_called_with( url="/networks/14/actions/change_ip_range", method="POST", json={"ip_range": "10.0.0.0/12"}, ) assert action.id == 1 assert action.progress == 0 class TestNetworksClient(object): @pytest.fixture() def networks_client(self): return NetworksClient(client=mock.MagicMock()) @pytest.fixture() def network_subnet(self): return NetworkSubnet( type=NetworkSubnet.TYPE_CLOUD, ip_range="10.0.1.0/24", network_zone="eu-central", ) @pytest.fixture() def network_vswitch_subnet(self): return NetworkSubnet( type=NetworkSubnet.TYPE_VSWITCH, ip_range="10.0.1.0/24", network_zone="eu-central", vswitch_id=123, ) @pytest.fixture() def network_route(self): return NetworkRoute(destination="10.100.1.0/24", gateway="10.0.1.1") def test_get_by_id(self, networks_client, network_response): networks_client._client.request.return_value = network_response bound_network = networks_client.get_by_id(1) networks_client._client.request.assert_called_with( url="/networks/1", method="GET" ) assert bound_network._client is networks_client assert bound_network.id == 1 assert bound_network.name == "mynet" @pytest.mark.parametrize( "params", [{"label_selector": "label1", "page": 1, "per_page": 10}, {"name": ""}, {}], ) def test_get_list(self, networks_client, two_networks_response, params): networks_client._client.request.return_value = two_networks_response result = networks_client.get_list(**params) networks_client._client.request.assert_called_with( url="/networks", method="GET", params=params ) bound_networks = result.networks assert result.meta is None assert len(bound_networks) == 2 bound_network1 = bound_networks[0] bound_network2 = bound_networks[1] assert bound_network1._client is networks_client assert bound_network1.id == 1 assert bound_network1.name == "mynet" assert bound_network2._client is networks_client assert bound_network2.id == 2 assert bound_network2.name == "myanothernet" @pytest.mark.parametrize("params", [{"label_selector": "label1"}]) def test_get_all(self, networks_client, two_networks_response, params): networks_client._client.request.return_value = two_networks_response bound_networks = networks_client.get_all(**params) params.update({"page": 1, "per_page": 50}) networks_client._client.request.assert_called_with( url="/networks", method="GET", params=params ) assert len(bound_networks) == 2 bound_network1 = bound_networks[0] bound_network2 = bound_networks[1] assert bound_network1._client is networks_client assert bound_network1.id == 1 assert bound_network1.name == "mynet" assert bound_network2._client is networks_client assert bound_network2.id == 2 assert bound_network2.name == "myanothernet" def test_get_by_name(self, networks_client, one_network_response): networks_client._client.request.return_value = one_network_response bound_network = networks_client.get_by_name("mynet") params = {"name": "mynet"} networks_client._client.request.assert_called_with( url="/networks", method="GET", params=params ) assert bound_network._client is networks_client assert bound_network.id == 1 assert bound_network.name == "mynet" def test_create(self, networks_client, network_create_response): networks_client._client.request.return_value = network_create_response networks_client.create(name="mynet", ip_range="10.0.0.0/8") networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", }, ) def test_create_with_subnet( self, networks_client, network_subnet, network_create_response ): networks_client._client.request.return_value = network_create_response networks_client.create( name="mynet", ip_range="10.0.0.0/8", subnets=[network_subnet] ) networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "subnets": [ { "type": NetworkSubnet.TYPE_CLOUD, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", } ], }, ) def test_create_with_route( self, networks_client, network_route, network_create_response ): networks_client._client.request.return_value = network_create_response networks_client.create( name="mynet", ip_range="10.0.0.0/8", routes=[network_route] ) networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "routes": [ { "destination": "10.100.1.0/24", "gateway": "10.0.1.1", } ], }, ) def test_create_with_route_and_subnet( self, networks_client, network_subnet, network_route, network_create_response ): networks_client._client.request.return_value = network_create_response networks_client.create( name="mynet", ip_range="10.0.0.0/8", subnets=[network_subnet], routes=[network_route], ) networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "subnets": [ { "type": NetworkSubnet.TYPE_CLOUD, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", } ], "routes": [ { "destination": "10.100.1.0/24", "gateway": "10.0.1.1", } ], }, ) @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_get_actions_list(self, networks_client, network, response_get_actions): networks_client._client.request.return_value = response_get_actions result = networks_client.get_actions_list(network, sort="id") networks_client._client.request.assert_called_with( url="/networks/1/actions", method="GET", params={"sort": "id"} ) actions = result.actions assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == networks_client._client.actions assert actions[0].id == 13 assert actions[0].command == "add_subnet" @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_update(self, networks_client, network, response_update_network): networks_client._client.request.return_value = response_update_network network = networks_client.update(network, name="new-name") networks_client._client.request.assert_called_with( url="/networks/1", method="PUT", json={"name": "new-name"} ) assert network.id == 4711 assert network.name == "new-name" @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_change_protection(self, networks_client, network, generic_action): networks_client._client.request.return_value = generic_action action = networks_client.change_protection(network, True) networks_client._client.request.assert_called_with( url="/networks/1/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_delete(self, networks_client, network, generic_action): networks_client._client.request.return_value = generic_action delete_success = networks_client.delete(network) networks_client._client.request.assert_called_with( url="/networks/1", method="DELETE" ) assert delete_success is True @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_add_subnet(self, networks_client, network, generic_action, network_subnet): networks_client._client.request.return_value = generic_action action = networks_client.add_subnet(network, network_subnet) networks_client._client.request.assert_called_with( url="/networks/1/actions/add_subnet", method="POST", json={ "type": NetworkSubnet.TYPE_CLOUD, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", }, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_add_subnet_vswitch( self, networks_client, network, generic_action, network_vswitch_subnet ): networks_client._client.request.return_value = generic_action action = networks_client.add_subnet(network, network_vswitch_subnet) networks_client._client.request.assert_called_with( url="/networks/1/actions/add_subnet", method="POST", json={ "type": NetworkSubnet.TYPE_VSWITCH, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "vswitch_id": 123, }, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_delete_subnet( self, networks_client, network, generic_action, network_subnet ): networks_client._client.request.return_value = generic_action action = networks_client.delete_subnet(network, network_subnet) networks_client._client.request.assert_called_with( url="/networks/1/actions/delete_subnet", method="POST", json={"ip_range": "10.0.1.0/24"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_add_route(self, networks_client, network, generic_action, network_route): networks_client._client.request.return_value = generic_action action = networks_client.add_route(network, network_route) networks_client._client.request.assert_called_with( url="/networks/1/actions/add_route", method="POST", json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_delete_route( self, networks_client, network, generic_action, network_route ): networks_client._client.request.return_value = generic_action action = networks_client.delete_route(network, network_route) networks_client._client.request.assert_called_with( url="/networks/1/actions/delete_route", method="POST", json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_change_ip_range(self, networks_client, network, generic_action): networks_client._client.request.return_value = generic_action action = networks_client.change_ip_range(network, "10.0.0.0/12") networks_client._client.request.assert_called_with( url="/networks/1/actions/change_ip_range", method="POST", json={"ip_range": "10.0.0.0/12"}, ) assert action.id == 1 assert action.progress == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/networks/test_domain.py0000644000175100001710000000053500000000000021275 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.networks.domain import Network class TestNetwork(object): def test_created_is_datetime(self): network = Network(id=1, created="2016-01-30T23:50+00:00") assert network.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8545113 hcloud-1.16.0/tests/unit/placement_groups/0000755000175100001710000000000000000000000020105 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/placement_groups/__init__.py0000644000175100001710000000000000000000000022204 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/placement_groups/conftest.py0000644000175100001710000000332200000000000022304 0ustar00runnerdockerimport pytest @pytest.fixture() def response_create_placement_group(): return { "placement_group": { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [], "type": "spread", } } @pytest.fixture() def one_placement_group_response(): return { "placement_groups": [ { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4711, 4712], "type": "spread", } ] } @pytest.fixture() def two_placement_groups_response(): return { "placement_groups": [ { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4711, 4712], "type": "spread", }, { "created": "2019-01-08T12:10:00+00:00", "id": 898, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4713, 4714, 4715], "type": "spread", }, ] } @pytest.fixture() def placement_group_response(): return { "placement_group": { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4711, 4712], "type": "spread", } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/placement_groups/test_client.py0000644000175100001710000001620700000000000023002 0ustar00runnerdockerimport pytest import mock from hcloud.placement_groups.client import BoundPlacementGroup, PlacementGroupsClient def check_variables(placement_group, expected): assert placement_group.id == expected["id"] assert placement_group.name == expected["name"] assert placement_group.labels == expected["labels"] assert placement_group.servers == expected["servers"] assert placement_group.type == expected["type"] class TestBoundPlacementGroup(object): @pytest.fixture() def bound_placement_group(self, hetzner_client): return BoundPlacementGroup( client=hetzner_client.placement_groups, data=dict(id=897) ) def test_bound_placement_group_init(self, placement_group_response): bound_placement_group = BoundPlacementGroup( client=mock.MagicMock(), data=placement_group_response["placement_group"] ) check_variables( bound_placement_group, placement_group_response["placement_group"] ) def test_update( self, hetzner_client, bound_placement_group, placement_group_response ): hetzner_client.request.return_value = placement_group_response placement_group = bound_placement_group.update( name=placement_group_response["placement_group"]["name"], labels=placement_group_response["placement_group"]["labels"], ) hetzner_client.request.assert_called_with( url="/placement_groups/{placement_group_id}".format( placement_group_id=placement_group_response["placement_group"]["id"] ), method="PUT", json={ "labels": placement_group_response["placement_group"]["labels"], "name": placement_group_response["placement_group"]["name"], }, ) check_variables(placement_group, placement_group_response["placement_group"]) def test_delete(self, hetzner_client, bound_placement_group): delete_success = bound_placement_group.delete() hetzner_client.request.assert_called_with( url="/placement_groups/897", method="DELETE" ) assert delete_success is True class TestPlacementGroupsClient(object): @pytest.fixture() def placement_groups_client(self): return PlacementGroupsClient(client=mock.MagicMock()) def test_get_by_id(self, placement_groups_client, placement_group_response): placement_groups_client._client.request.return_value = placement_group_response placement_group = placement_groups_client.get_by_id( placement_group_response["placement_group"]["id"] ) placement_groups_client._client.request.assert_called_with( url="/placement_groups/{placement_group_id}".format( placement_group_id=placement_group_response["placement_group"]["id"] ), method="GET", ) assert placement_group._client is placement_groups_client check_variables(placement_group, placement_group_response["placement_group"]) @pytest.mark.parametrize( "params", [ { "name": "my Placement Group", "sort": "id", "label_selector": "key==value", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list( self, placement_groups_client, two_placement_groups_response, params ): placement_groups_client._client.request.return_value = ( two_placement_groups_response ) result = placement_groups_client.get_list(**params) placement_groups_client._client.request.assert_called_with( url="/placement_groups", method="GET", params=params ) placement_groups = result.placement_groups assert result.meta is None assert len(placement_groups) == len( two_placement_groups_response["placement_groups"] ) for placement_group, expected in zip( placement_groups, two_placement_groups_response["placement_groups"] ): assert placement_group._client is placement_groups_client check_variables(placement_group, expected) @pytest.mark.parametrize( "params", [ { "name": "Corporate Intranet Protection", "sort": "id", "label_selector": "key==value", }, {}, ], ) def test_get_all( self, placement_groups_client, two_placement_groups_response, params ): placement_groups_client._client.request.return_value = ( two_placement_groups_response ) placement_groups = placement_groups_client.get_all(**params) params.update({"page": 1, "per_page": 50}) placement_groups_client._client.request.assert_called_with( url="/placement_groups", method="GET", params=params ) assert len(placement_groups) == len( two_placement_groups_response["placement_groups"] ) for placement_group, expected in zip( placement_groups, two_placement_groups_response["placement_groups"] ): assert placement_group._client is placement_groups_client check_variables(placement_group, expected) def test_get_by_name(self, placement_groups_client, one_placement_group_response): placement_groups_client._client.request.return_value = ( one_placement_group_response ) placement_group = placement_groups_client.get_by_name( one_placement_group_response["placement_groups"][0]["name"] ) params = {"name": one_placement_group_response["placement_groups"][0]["name"]} placement_groups_client._client.request.assert_called_with( url="/placement_groups", method="GET", params=params ) check_variables( placement_group, one_placement_group_response["placement_groups"][0] ) def test_create(self, placement_groups_client, response_create_placement_group): placement_groups_client._client.request.return_value = ( response_create_placement_group ) response = placement_groups_client.create( name=response_create_placement_group["placement_group"]["name"], type=response_create_placement_group["placement_group"]["type"], labels=response_create_placement_group["placement_group"]["labels"], ) json = { "name": response_create_placement_group["placement_group"]["name"], "labels": response_create_placement_group["placement_group"]["labels"], "type": response_create_placement_group["placement_group"]["type"], } placement_groups_client._client.request.assert_called_with( url="/placement_groups", method="POST", json=json ) bound_placement_group = response.placement_group assert bound_placement_group._client is placement_groups_client check_variables( bound_placement_group, response_create_placement_group["placement_group"] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/placement_groups/test_domain.py0000644000175100001710000000061200000000000022764 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.placement_groups.domain import PlacementGroup class TestPlacementGroup(object): def test_created_is_datetime(self): placement_group = PlacementGroup(id=1, created="2016-01-30T23:50+00:00") assert placement_group.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8545113 hcloud-1.16.0/tests/unit/server_types/0000755000175100001710000000000000000000000017270 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/server_types/__init__.py0000644000175100001710000000000000000000000021367 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/server_types/conftest.py0000644000175100001710000000733100000000000021473 0ustar00runnerdockerimport pytest @pytest.fixture() def server_type_response(): return { "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", } } @pytest.fixture() def two_server_types_response(): return { "server_types": [ { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, { "id": 2, "name": "cx21", "description": "CX21", "cores": 2, "memory": 4.0, "disk": 40, "prices": [ { "location": "fsn1", "price_hourly": { "net": "0.0080000000", "gross": "0.0095200000000000", }, "price_monthly": { "net": "4.9000000000", "gross": "5.8310000000000000", }, }, { "location": "nbg1", "price_hourly": { "net": "0.0080000000", "gross": "0.0095200000000000", }, "price_monthly": { "net": "4.9000000000", "gross": "5.8310000000000000", }, }, ], "storage_type": "local", "cpu_type": "shared", }, ] } @pytest.fixture() def one_server_types_response(): return { "server_types": [ { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/server_types/test_client.py0000644000175100001710000000601500000000000022161 0ustar00runnerdockerimport pytest import mock from hcloud.server_types.client import ServerTypesClient class TestServerTypesClient(object): @pytest.fixture() def server_types_client(self): return ServerTypesClient(client=mock.MagicMock()) def test_get_by_id(self, server_types_client, server_type_response): server_types_client._client.request.return_value = server_type_response server_type = server_types_client.get_by_id(1) server_types_client._client.request.assert_called_with( url="/server_types/1", method="GET" ) assert server_type._client is server_types_client assert server_type.id == 1 assert server_type.name == "cx11" @pytest.mark.parametrize( "params", [{"name": "cx11", "page": 1, "per_page": 10}, {"name": ""}, {}] ) def test_get_list(self, server_types_client, two_server_types_response, params): server_types_client._client.request.return_value = two_server_types_response result = server_types_client.get_list(**params) server_types_client._client.request.assert_called_with( url="/server_types", method="GET", params=params ) server_types = result.server_types assert result.meta is None assert len(server_types) == 2 server_types1 = server_types[0] server_types2 = server_types[1] assert server_types1._client is server_types_client assert server_types1.id == 1 assert server_types1.name == "cx11" assert server_types2._client is server_types_client assert server_types2.id == 2 assert server_types2.name == "cx21" @pytest.mark.parametrize("params", [{"name": "cx11"}]) def test_get_all(self, server_types_client, two_server_types_response, params): server_types_client._client.request.return_value = two_server_types_response server_types = server_types_client.get_all(**params) params.update({"page": 1, "per_page": 50}) server_types_client._client.request.assert_called_with( url="/server_types", method="GET", params=params ) assert len(server_types) == 2 server_types1 = server_types[0] server_types2 = server_types[1] assert server_types1._client is server_types_client assert server_types1.id == 1 assert server_types1.name == "cx11" assert server_types2._client is server_types_client assert server_types2.id == 2 assert server_types2.name == "cx21" def test_get_by_name(self, server_types_client, one_server_types_response): server_types_client._client.request.return_value = one_server_types_response server_type = server_types_client.get_by_name("cx11") params = {"name": "cx11"} server_types_client._client.request.assert_called_with( url="/server_types", method="GET", params=params ) assert server_type._client is server_types_client assert server_type.id == 1 assert server_type.name == "cx11" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8545113 hcloud-1.16.0/tests/unit/servers/0000755000175100001710000000000000000000000016227 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/servers/__init__.py0000644000175100001710000000000000000000000020326 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/servers/conftest.py0000644000175100001710000007207300000000000020437 0ustar00runnerdockerimport pytest @pytest.fixture() def response_simple_server(): return { "server": { "id": 1, "name": "my-server", "status": "running", "created": "2016-01-30T23:50+00:00", "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": False, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], }, "floating_ips": [478], "firewalls": [{"id": 38, "status": "applied"}], }, "private_net": [ { "network": 4711, "ip": "10.1.1.5", "alias_ips": ["10.1.1.8"], "mac_address": "86:00:ff:2a:7d:e1", } ], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False, "rebuild": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": None, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "primary_disk_size": 20, "protection": {}, "labels": {}, "volumes": [], } } @pytest.fixture() def response_create_simple_server(): return { "server": { "id": 1, "name": "my-server", "status": "running", "created": "2016-01-30T23:50+00:00", "primary_disk_size": 20, "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": False, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], }, "floating_ips": [], "firewalls": [{"id": 38, "status": "applied"}], }, "private_net": [], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False, "rebuild": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": { "id": 4711, }, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "protection": {}, "labels": {}, "volumes": [], }, "action": { "id": 1, "command": "create_server", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, "next_actions": [ { "id": 13, "command": "start_server", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } ], "root_password": "YItygq1v3GYjjMomLaKc", } @pytest.fixture() def response_update_server(): return { "server": { "id": 14, "name": "new-name", "status": "running", "created": "2016-01-30T23:50+00:00", "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": False, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], }, "floating_ips": [478], "firewalls": [], }, "private_net": [], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00", }, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "protection": {"delete": False, "rebuild": False}, "labels": {}, "volumes": [], } } @pytest.fixture() def response_simple_servers(): return { "servers": [ { "id": 1, "name": "my-server", "status": "running", "created": "2016-01-30T23:50+00:00", "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": False, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, "dns_ptr": [ {"ip": "2001:db8::1", "dns_ptr": "server.example.com"} ], }, "floating_ips": [478], "firewalls": [], }, "private_net": [ { "network": 4711, "ip": "10.1.1.5", "alias_ips": ["10.1.1.8"], "mac_address": "86:00:ff:2a:7d:e1", } ], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False, "rebuild": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": None, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "protection": {}, "labels": {}, "volumes": [], }, { "id": 2, "name": "my-server2", "status": "running", "created": "2016-03-30T23:50+00:00", "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": False, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, "dns_ptr": [ {"ip": "2001:db8::1", "dns_ptr": "server.example.com"} ], }, "floating_ips": [478], "firewalls": [], }, "private_net": [ { "network": 4711, "ip": "10.1.1.7", "alias_ips": ["10.1.1.99"], "mac_address": "86:00:ff:2a:7d:e1", } ], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False, "rebuild": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": None, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "primary_disk_size": 20, "protection": {}, "labels": {}, "volumes": [], }, ] } @pytest.fixture() def response_full_server(): return { "server": { "id": 42, "name": "my-server", "status": "running", "created": "2016-01-30T23:50+00:00", "primary_disk_size": 20, "public_net": { "ipv4": { "ip": "1.2.3.4", "blocked": False, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], }, "floating_ips": [478], "firewalls": [{"id": 38, "status": "applied"}], }, "private_net": [ { "network": 4711, "ip": "10.1.1.5", "alias_ips": ["10.1.1.8"], "mac_address": "86:00:ff:2a:7d:e1", } ], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00", }, "placement_group": { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4711, 4712], "type": "spread", }, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "protection": {}, "labels": {}, "volumes": [1, 2], } } @pytest.fixture() def response_server_reset_password(): return { "action": { "id": 1, "command": "reset_password", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, "root_password": "YItygq1v3GYjjMomLaKc", } @pytest.fixture() def response_server_enable_rescue(): return { "action": { "id": 1, "command": "enable_rescue", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, "root_password": "YItygq1v3GYjjMomLaKc", } @pytest.fixture() def response_server_create_image(): return { "image": { "id": 4711, "type": "snapshot", "status": "creating", "name": None, "description": "my image", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "action": { "id": 1, "command": "enable_rescue", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, } @pytest.fixture() def response_server_request_console(): return { "wss_url": "wss://console.hetzner.cloud/?server_id=1&token=3db32d15-af2f-459c-8bf8-dee1fd05f49c", "password": "9MQaTg2VAGI0FIpc10k3UpRXcHj2wQ6x", "action": { "id": 1, "command": "request_console", "status": "success", "progress": 0, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "start_server", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } @pytest.fixture() def response_attach_to_network(): return { "action": { "id": 1, "command": "attach_to_network", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [ {"id": 42, "type": "server"}, {"id": 4711, "type": "network"}, ], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_detach_from_network(): return { "action": { "id": 1, "command": "detach_from_network", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [ {"id": 42, "type": "server"}, {"id": 4711, "type": "network"}, ], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_change_alias_ips(): return { "action": { "id": 1, "command": "change_alias_ips", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [ {"id": 42, "type": "server"}, {"id": 4711, "type": "network"}, ], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_apply_firewall(): return { "action": { "id": 1, "command": "apply_firewall", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_remove_firewall(): return { "action": { "id": 1, "command": "remove_firewall", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_add_to_placement_group(): return { "action": { "command": "add_to_placement_group", "error": {"code": "action_failed", "message": "Action failed"}, "finished": None, "id": 13, "progress": 0, "resources": [{"id": 42, "type": "server"}], "started": "2016-01-30T23:50:00+00:00", "status": "running", } } @pytest.fixture() def response_remove_from_placement_group(): return { "action": { "command": "remove_from_placement_group", "error": {"code": "action_failed", "message": "Action failed"}, "finished": "2016-01-30T23:56:00+00:00", "id": 13, "progress": 100, "resources": [{"id": 42, "type": "server"}], "started": "2016-01-30T23:55:00+00:00", "status": "success", } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/servers/test_client.py0000644000175100001710000013343600000000000021130 0ustar00runnerdockerimport mock import pytest from hcloud.firewalls.client import BoundFirewall from hcloud.firewalls.domain import Firewall from hcloud.floating_ips.client import BoundFloatingIP from hcloud.isos.client import BoundIso from hcloud.servers.client import ServersClient, BoundServer from hcloud.servers.domain import ( Server, PublicNetwork, IPv4Address, IPv6Network, PublicNetworkFirewall, PrivateNet, ) from hcloud.volumes.client import BoundVolume from hcloud.volumes.domain import Volume from hcloud.images.domain import Image from hcloud.images.client import BoundImage from hcloud.isos.domain import Iso from hcloud.datacenters.client import BoundDatacenter from hcloud.datacenters.domain import Datacenter from hcloud.locations.domain import Location from hcloud.actions.client import BoundAction from hcloud.server_types.client import BoundServerType from hcloud.server_types.domain import ServerType from hcloud.networks.domain import Network from hcloud.networks.client import BoundNetwork from hcloud.placement_groups.domain import PlacementGroup from hcloud.placement_groups.client import BoundPlacementGroup class TestBoundServer(object): @pytest.fixture() def bound_server(self, hetzner_client): return BoundServer(client=hetzner_client.servers, data=dict(id=14)) def test_bound_server_init(self, response_full_server): bound_server = BoundServer( client=mock.MagicMock(), data=response_full_server["server"] ) assert bound_server.id == 42 assert bound_server.name == "my-server" assert bound_server.primary_disk_size == 20 assert isinstance(bound_server.public_net, PublicNetwork) assert isinstance(bound_server.public_net.ipv4, IPv4Address) assert bound_server.public_net.ipv4.ip == "1.2.3.4" assert bound_server.public_net.ipv4.blocked is False assert bound_server.public_net.ipv4.dns_ptr == "server01.example.com" assert isinstance(bound_server.public_net.ipv6, IPv6Network) assert bound_server.public_net.ipv6.ip == "2001:db8::/64" assert bound_server.public_net.ipv6.blocked is False assert bound_server.public_net.ipv6.network == "2001:db8::" assert bound_server.public_net.ipv6.network_mask == "64" assert isinstance(bound_server.public_net.firewalls, list) assert isinstance(bound_server.public_net.firewalls[0], PublicNetworkFirewall) firewall = bound_server.public_net.firewalls[0] assert isinstance(firewall.firewall, BoundFirewall) assert bound_server.public_net.ipv6.blocked is False assert firewall.status == PublicNetworkFirewall.STATUS_APPLIED assert isinstance(bound_server.public_net.floating_ips[0], BoundFloatingIP) assert bound_server.public_net.floating_ips[0].id == 478 assert bound_server.public_net.floating_ips[0].complete is False assert isinstance(bound_server.datacenter, BoundDatacenter) assert ( bound_server.datacenter._client == bound_server._client._client.datacenters ) assert bound_server.datacenter.id == 1 assert bound_server.datacenter.complete is True assert isinstance(bound_server.server_type, BoundServerType) assert ( bound_server.server_type._client == bound_server._client._client.server_types ) assert bound_server.server_type.id == 1 assert bound_server.server_type.complete is True assert len(bound_server.volumes) == 2 assert isinstance(bound_server.volumes[0], BoundVolume) assert bound_server.volumes[0]._client == bound_server._client._client.volumes assert bound_server.volumes[0].id == 1 assert bound_server.volumes[0].complete is False assert isinstance(bound_server.volumes[1], BoundVolume) assert bound_server.volumes[1]._client == bound_server._client._client.volumes assert bound_server.volumes[1].id == 2 assert bound_server.volumes[1].complete is False assert isinstance(bound_server.image, BoundImage) assert bound_server.image._client == bound_server._client._client.images assert bound_server.image.id == 4711 assert bound_server.image.name == "ubuntu-20.04" assert bound_server.image.complete is True assert isinstance(bound_server.iso, BoundIso) assert bound_server.iso._client == bound_server._client._client.isos assert bound_server.iso.id == 4711 assert bound_server.iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" assert bound_server.iso.complete is True assert len(bound_server.private_net) == 1 assert isinstance(bound_server.private_net[0], PrivateNet) assert ( bound_server.private_net[0].network._client == bound_server._client._client.networks ) assert bound_server.private_net[0].ip == "10.1.1.5" assert bound_server.private_net[0].mac_address == "86:00:ff:2a:7d:e1" assert len(bound_server.private_net[0].alias_ips) == 1 assert bound_server.private_net[0].alias_ips[0] == "10.1.1.8" assert isinstance(bound_server.placement_group, BoundPlacementGroup) assert ( bound_server.placement_group._client == bound_server._client._client.placement_groups ) assert bound_server.placement_group.id == 897 assert bound_server.placement_group.name == "my Placement Group" assert bound_server.placement_group.complete is True @pytest.mark.parametrize( "params", [ { "status": [Server.STATUS_RUNNING], "sort": "status", "page": 1, "per_page": 10, }, {}, ], ) def test_get_actions_list( self, hetzner_client, bound_server, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_server.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/servers/14/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "start_server" @pytest.mark.parametrize( "params", [{"status": [Server.STATUS_RUNNING], "sort": "status"}, {}] ) def test_get_actions( self, hetzner_client, bound_server, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions actions = bound_server.get_actions(**params) params.update({"page": 1, "per_page": 50}) hetzner_client.request.assert_called_with( url="/servers/14/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "start_server" def test_update(self, hetzner_client, bound_server, response_update_server): hetzner_client.request.return_value = response_update_server server = bound_server.update(name="new-name", labels={}) hetzner_client.request.assert_called_with( url="/servers/14", method="PUT", json={"name": "new-name", "labels": {}} ) assert server.id == 14 assert server.name == "new-name" def test_delete(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.delete() hetzner_client.request.assert_called_with(url="/servers/14", method="DELETE") assert action.id == 1 assert action.progress == 0 def test_power_off(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.power_off() hetzner_client.request.assert_called_with( url="/servers/14/actions/poweroff", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_power_on(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.power_on() hetzner_client.request.assert_called_with( url="/servers/14/actions/poweron", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_reboot(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.reboot() hetzner_client.request.assert_called_with( url="/servers/14/actions/reboot", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_reset(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.reset() hetzner_client.request.assert_called_with( url="/servers/14/actions/reset", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_shutdown(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.shutdown() hetzner_client.request.assert_called_with( url="/servers/14/actions/shutdown", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_reset_password( self, hetzner_client, bound_server, response_server_reset_password ): hetzner_client.request.return_value = response_server_reset_password response = bound_server.reset_password() hetzner_client.request.assert_called_with( url="/servers/14/actions/reset_password", method="POST" ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password == "YItygq1v3GYjjMomLaKc" def test_change_type(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.change_type(ServerType(name="cx11"), upgrade_disk=True) hetzner_client.request.assert_called_with( url="/servers/14/actions/change_type", method="POST", json={"server_type": "cx11", "upgrade_disk": True}, ) assert action.id == 1 assert action.progress == 0 def test_enable_rescue( self, hetzner_client, bound_server, response_server_enable_rescue ): hetzner_client.request.return_value = response_server_enable_rescue response = bound_server.enable_rescue(type="linux64") hetzner_client.request.assert_called_with( url="/servers/14/actions/enable_rescue", method="POST", json={"type": "linux64"}, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password == "YItygq1v3GYjjMomLaKc" def test_disable_rescue(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.disable_rescue() hetzner_client.request.assert_called_with( url="/servers/14/actions/disable_rescue", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_create_image( self, hetzner_client, bound_server, response_server_create_image ): hetzner_client.request.return_value = response_server_create_image response = bound_server.create_image(description="my image", type="snapshot") hetzner_client.request.assert_called_with( url="/servers/14/actions/create_image", method="POST", json={"description": "my image", "type": "snapshot"}, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.image.description == "my image" def test_rebuild(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.rebuild(Image(name="ubuntu-20.04")) hetzner_client.request.assert_called_with( url="/servers/14/actions/rebuild", method="POST", json={"image": "ubuntu-20.04"}, ) assert action.id == 1 assert action.progress == 0 def test_enable_backup(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.enable_backup() hetzner_client.request.assert_called_with( url="/servers/14/actions/enable_backup", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_disable_backup(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.disable_backup() hetzner_client.request.assert_called_with( url="/servers/14/actions/disable_backup", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_attach_iso(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.attach_iso(Iso(name="FreeBSD-11.0-RELEASE-amd64-dvd1")) hetzner_client.request.assert_called_with( url="/servers/14/actions/attach_iso", method="POST", json={"iso": "FreeBSD-11.0-RELEASE-amd64-dvd1"}, ) assert action.id == 1 assert action.progress == 0 def test_detach_iso(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.detach_iso() hetzner_client.request.assert_called_with( url="/servers/14/actions/detach_iso", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_change_dns_ptr(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.change_dns_ptr("1.2.3.4", "example.com") hetzner_client.request.assert_called_with( url="/servers/14/actions/change_dns_ptr", method="POST", json={"ip": "1.2.3.4", "dns_ptr": "example.com"}, ) assert action.id == 1 assert action.progress == 0 def test_change_protection(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.change_protection(True, True) hetzner_client.request.assert_called_with( url="/servers/14/actions/change_protection", method="POST", json={"delete": True, "rebuild": True}, ) assert action.id == 1 assert action.progress == 0 def test_request_console( self, hetzner_client, bound_server, response_server_request_console ): hetzner_client.request.return_value = response_server_request_console response = bound_server.request_console() hetzner_client.request.assert_called_with( url="/servers/14/actions/request_console", method="POST" ) assert response.action.id == 1 assert response.action.progress == 0 assert ( response.wss_url == "wss://console.hetzner.cloud/?server_id=1&token=3db32d15-af2f-459c-8bf8-dee1fd05f49c" ) assert response.password == "9MQaTg2VAGI0FIpc10k3UpRXcHj2wQ6x" @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_attach_to_network( self, hetzner_client, bound_server, network, response_attach_to_network ): hetzner_client.request.return_value = response_attach_to_network action = bound_server.attach_to_network( network, "10.0.1.1", ["10.0.1.2", "10.0.1.3"] ) hetzner_client.request.assert_called_with( url="/servers/14/actions/attach_to_network", method="POST", json={ "network": 4711, "ip": "10.0.1.1", "alias_ips": ["10.0.1.2", "10.0.1.3"], }, ) assert action.id == 1 assert action.progress == 0 assert action.command == "attach_to_network" @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_detach_from_network( self, hetzner_client, bound_server, network, response_detach_from_network ): hetzner_client.request.return_value = response_detach_from_network action = bound_server.detach_from_network(network) hetzner_client.request.assert_called_with( url="/servers/14/actions/detach_from_network", method="POST", json={"network": 4711}, ) assert action.id == 1 assert action.progress == 0 assert action.command == "detach_from_network" @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_change_alias_ips( self, hetzner_client, bound_server, network, response_change_alias_ips ): hetzner_client.request.return_value = response_change_alias_ips action = bound_server.change_alias_ips(network, ["10.0.1.2", "10.0.1.3"]) hetzner_client.request.assert_called_with( url="/servers/14/actions/change_alias_ips", method="POST", json={"network": 4711, "alias_ips": ["10.0.1.2", "10.0.1.3"]}, ) assert action.id == 1 assert action.progress == 0 assert action.command == "change_alias_ips" @pytest.mark.parametrize( "placement_group", [PlacementGroup(id=897), BoundPlacementGroup(mock.MagicMock, dict(id=897))], ) def test_add_to_placement_group( self, hetzner_client, bound_server, placement_group, response_add_to_placement_group, ): hetzner_client.request.return_value = response_add_to_placement_group action = bound_server.add_to_placement_group(placement_group) hetzner_client.request.assert_called_with( url="/servers/14/actions/add_to_placement_group", method="POST", json={"placement_group": "897"}, ) assert action.id == 13 assert action.progress == 0 assert action.command == "add_to_placement_group" def test_remove_from_placement_group( self, hetzner_client, bound_server, response_remove_from_placement_group ): hetzner_client.request.return_value = response_remove_from_placement_group action = bound_server.remove_from_placement_group() hetzner_client.request.assert_called_with( url="/servers/14/actions/remove_from_placement_group", method="POST" ) assert action.id == 13 assert action.progress == 100 assert action.command == "remove_from_placement_group" class TestServersClient(object): @pytest.fixture() def servers_client(self): return ServersClient(client=mock.MagicMock()) def test_get_by_id(self, servers_client, response_simple_server): servers_client._client.request.return_value = response_simple_server bound_server = servers_client.get_by_id(1) servers_client._client.request.assert_called_with( url="/servers/1", method="GET" ) assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" @pytest.mark.parametrize( "params", [ {"name": "server1", "label_selector": "label1", "page": 1, "per_page": 10}, {"name": ""}, {}, ], ) def test_get_list(self, servers_client, response_simple_servers, params): servers_client._client.request.return_value = response_simple_servers result = servers_client.get_list(**params) servers_client._client.request.assert_called_with( url="/servers", method="GET", params=params ) bound_servers = result.servers assert result.meta is None assert len(bound_servers) == 2 bound_server1 = bound_servers[0] bound_server2 = bound_servers[1] assert bound_server1._client is servers_client assert bound_server1.id == 1 assert bound_server1.name == "my-server" assert bound_server2._client is servers_client assert bound_server2.id == 2 assert bound_server2.name == "my-server2" @pytest.mark.parametrize( "params", [{"name": "server1", "label_selector": "label1"}, {}] ) def test_get_all(self, servers_client, response_simple_servers, params): servers_client._client.request.return_value = response_simple_servers bound_servers = servers_client.get_all(**params) params.update({"page": 1, "per_page": 50}) servers_client._client.request.assert_called_with( url="/servers", method="GET", params=params ) assert len(bound_servers) == 2 bound_server1 = bound_servers[0] bound_server2 = bound_servers[1] assert bound_server1._client is servers_client assert bound_server1.id == 1 assert bound_server1.name == "my-server" assert bound_server2._client is servers_client assert bound_server2.id == 2 assert bound_server2.name == "my-server2" def test_get_by_name(self, servers_client, response_simple_servers): servers_client._client.request.return_value = response_simple_servers bound_server = servers_client.get_by_name("my-server") params = {"name": "my-server"} servers_client._client.request.assert_called_with( url="/servers", method="GET", params=params ) assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" def test_create_with_datacenter( self, servers_client, response_create_simple_server ): servers_client._client.request.return_value = response_create_simple_server response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), datacenter=Datacenter(id=1), ) servers_client._client.request.assert_called_with( url="/servers", method="POST", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "datacenter": 1, "start_after_create": True, }, ) bound_server = response.server assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" def test_create_with_location(self, servers_client, response_create_simple_server): servers_client._client.request.return_value = response_create_simple_server response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(name="ubuntu-20.04"), location=Location(name="fsn1"), ) servers_client._client.request.assert_called_with( url="/servers", method="POST", json={ "name": "my-server", "server_type": "cx11", "image": "ubuntu-20.04", "location": "fsn1", "start_after_create": True, }, ) bound_server = response.server bound_action = response.action assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" assert isinstance(bound_action, BoundAction) assert bound_action._client == servers_client._client.actions assert bound_action.id == 1 assert bound_action.command == "create_server" def test_create_with_volumes(self, servers_client, response_create_simple_server): servers_client._client.request.return_value = response_create_simple_server volumes = [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=2))] response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), volumes=volumes, start_after_create=False, ) servers_client._client.request.assert_called_with( url="/servers", method="POST", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "volumes": [1, 2], "start_after_create": False, }, ) bound_server = response.server bound_action = response.action next_actions = response.next_actions root_password = response.root_password assert root_password == "YItygq1v3GYjjMomLaKc" assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" assert isinstance(bound_action, BoundAction) assert bound_action._client == servers_client._client.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 def test_create_with_networks(self, servers_client, response_create_simple_server): servers_client._client.request.return_value = response_create_simple_server networks = [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=2))] response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), networks=networks, start_after_create=False, ) servers_client._client.request.assert_called_with( url="/servers", method="POST", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "networks": [1, 2], "start_after_create": False, }, ) bound_server = response.server bound_action = response.action next_actions = response.next_actions root_password = response.root_password assert root_password == "YItygq1v3GYjjMomLaKc" assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" assert isinstance(bound_action, BoundAction) assert bound_action._client == servers_client._client.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 def test_create_with_firewalls(self, servers_client, response_create_simple_server): servers_client._client.request.return_value = response_create_simple_server firewalls = [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=2))] response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), firewalls=firewalls, start_after_create=False, ) servers_client._client.request.assert_called_with( url="/servers", method="POST", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "firewalls": [{"firewall": 1}, {"firewall": 2}], "start_after_create": False, }, ) bound_server = response.server bound_action = response.action next_actions = response.next_actions root_password = response.root_password assert root_password == "YItygq1v3GYjjMomLaKc" assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" assert isinstance(bound_action, BoundAction) assert bound_action._client == servers_client._client.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 def test_create_with_placement_group( self, servers_client, response_create_simple_server ): servers_client._client.request.return_value = response_create_simple_server placement_group = PlacementGroup(id=1) response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), start_after_create=False, placement_group=placement_group, ) servers_client._client.request.assert_called_with( url="/servers", method="POST", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "placement_group": 1, "start_after_create": False, }, ) bound_server = response.server bound_action = response.action next_actions = response.next_actions root_password = response.root_password assert root_password == "YItygq1v3GYjjMomLaKc" assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" assert isinstance(bound_action, BoundAction) assert bound_action._client == servers_client._client.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_get_actions_list(self, servers_client, server, response_get_actions): servers_client._client.request.return_value = response_get_actions result = servers_client.get_actions_list(server) servers_client._client.request.assert_called_with( url="/servers/1/actions", method="GET", params={} ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == servers_client._client.actions assert actions[0].id == 13 assert actions[0].command == "start_server" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_update(self, servers_client, server, response_update_server): servers_client._client.request.return_value = response_update_server server = servers_client.update(server, name="new-name", labels={}) servers_client._client.request.assert_called_with( url="/servers/1", method="PUT", json={"name": "new-name", "labels": {}} ) assert server.id == 14 assert server.name == "new-name" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_delete(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.delete(server) servers_client._client.request.assert_called_with( url="/servers/1", method="DELETE" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_power_off(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.power_off(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/poweroff", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_power_on(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.power_on(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/poweron", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_reboot(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.reboot(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/reboot", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_reset(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.reset(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/reset", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_shutdown(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.shutdown(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/shutdown", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_reset_password( self, servers_client, server, response_server_reset_password ): servers_client._client.request.return_value = response_server_reset_password response = servers_client.reset_password(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/reset_password", method="POST" ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password == "YItygq1v3GYjjMomLaKc" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_type_with_server_type_name( self, servers_client, server, generic_action ): servers_client._client.request.return_value = generic_action action = servers_client.change_type( server, ServerType(name="cx11"), upgrade_disk=True ) servers_client._client.request.assert_called_with( url="/servers/1/actions/change_type", method="POST", json={"server_type": "cx11", "upgrade_disk": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_type_with_server_type_id( self, servers_client, server, generic_action ): servers_client._client.request.return_value = generic_action action = servers_client.change_type(server, ServerType(id=1), upgrade_disk=True) servers_client._client.request.assert_called_with( url="/servers/1/actions/change_type", method="POST", json={"server_type": 1, "upgrade_disk": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_type_with_blank_server_type(self, servers_client, server): with pytest.raises(ValueError) as e: servers_client.change_type(server, ServerType(), upgrade_disk=True) assert str(e.value) == "id or name must be set" servers_client._client.request.assert_not_called() @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_enable_rescue(self, servers_client, server, response_server_enable_rescue): servers_client._client.request.return_value = response_server_enable_rescue response = servers_client.enable_rescue(server, "linux64", [2323]) servers_client._client.request.assert_called_with( url="/servers/1/actions/enable_rescue", method="POST", json={"type": "linux64", "ssh_keys": [2323]}, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password == "YItygq1v3GYjjMomLaKc" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_disable_rescue(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.disable_rescue(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/disable_rescue", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_create_image(self, servers_client, server, response_server_create_image): servers_client._client.request.return_value = response_server_create_image response = servers_client.create_image( server, description="my image", type="snapshot", labels={"key": "value"} ) servers_client._client.request.assert_called_with( url="/servers/1/actions/create_image", method="POST", json={ "description": "my image", "type": "snapshot", "labels": {"key": "value"}, }, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.image.description == "my image" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_rebuild(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.rebuild(server, Image(name="ubuntu-20.04")) servers_client._client.request.assert_called_with( url="/servers/1/actions/rebuild", method="POST", json={"image": "ubuntu-20.04"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_enable_backup(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.enable_backup(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/enable_backup", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_disable_backup(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.disable_backup(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/disable_backup", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_attach_iso(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.attach_iso( server, Iso(name="FreeBSD-11.0-RELEASE-amd64-dvd1") ) servers_client._client.request.assert_called_with( url="/servers/1/actions/attach_iso", method="POST", json={"iso": "FreeBSD-11.0-RELEASE-amd64-dvd1"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_detach_iso(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.detach_iso(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/detach_iso", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_dns_ptr(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.change_dns_ptr(server, "1.2.3.4", "example.com") servers_client._client.request.assert_called_with( url="/servers/1/actions/change_dns_ptr", method="POST", json={"ip": "1.2.3.4", "dns_ptr": "example.com"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_protection(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.change_protection(server, True, True) servers_client._client.request.assert_called_with( url="/servers/1/actions/change_protection", method="POST", json={"delete": True, "rebuild": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_request_console( self, servers_client, server, response_server_request_console ): servers_client._client.request.return_value = response_server_request_console response = servers_client.request_console(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/request_console", method="POST" ) assert response.action.id == 1 assert response.action.progress == 0 assert ( response.wss_url == "wss://console.hetzner.cloud/?server_id=1&token=3db32d15-af2f-459c-8bf8-dee1fd05f49c" ) assert response.password == "9MQaTg2VAGI0FIpc10k3UpRXcHj2wQ6x" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_attach_to_network( self, servers_client, server, network, response_attach_to_network ): servers_client._client.request.return_value = response_attach_to_network action = servers_client.attach_to_network( server, network, "10.0.1.1", ["10.0.1.2", "10.0.1.3"] ) servers_client._client.request.assert_called_with( url="/servers/1/actions/attach_to_network", method="POST", json={ "network": 4711, "ip": "10.0.1.1", "alias_ips": ["10.0.1.2", "10.0.1.3"], }, ) assert action.id == 1 assert action.progress == 0 assert action.command == "attach_to_network" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_detach_from_network( self, servers_client, server, network, response_detach_from_network ): servers_client._client.request.return_value = response_detach_from_network action = servers_client.detach_from_network(server, network) servers_client._client.request.assert_called_with( url="/servers/1/actions/detach_from_network", method="POST", json={"network": 4711}, ) assert action.id == 1 assert action.progress == 0 assert action.command == "detach_from_network" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_change_alias_ips( self, servers_client, server, network, response_change_alias_ips ): servers_client._client.request.return_value = response_change_alias_ips action = servers_client.change_alias_ips( server, network, ["10.0.1.2", "10.0.1.3"] ) servers_client._client.request.assert_called_with( url="/servers/1/actions/change_alias_ips", method="POST", json={"network": 4711, "alias_ips": ["10.0.1.2", "10.0.1.3"]}, ) assert action.id == 1 assert action.progress == 0 assert action.command == "change_alias_ips" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/servers/test_domain.py0000644000175100001710000000052700000000000021113 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.servers.domain import Server class TestServer(object): def test_created_is_datetime(self): server = Server(id=1, created="2016-01-30T23:50+00:00") assert server.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8545113 hcloud-1.16.0/tests/unit/ssh_keys/0000755000175100001710000000000000000000000016366 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/ssh_keys/__init__.py0000644000175100001710000000000000000000000020465 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/ssh_keys/conftest.py0000644000175100001710000000346000000000000020570 0ustar00runnerdockerimport pytest @pytest.fixture() def ssh_key_response(): return { "ssh_key": { "id": 2323, "name": "My ssh key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, "created": "2016-01-30T23:50:00+00:00", } } @pytest.fixture() def two_ssh_keys_response(): return { "ssh_keys": [ { "id": 2323, "name": "SSH-Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, "created": "2016-01-30T23:50:00+00:00", }, { "id": 2324, "name": "SSH-Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, "created": "2016-01-30T23:50:00+00:00", }, ] } @pytest.fixture() def one_ssh_keys_response(): return { "ssh_keys": [ { "id": 2323, "name": "SSH-Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, } ] } @pytest.fixture() def response_update_ssh_key(): return { "ssh_key": { "id": 2323, "name": "New name", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, "created": "2016-01-30T23:50:00+00:00", } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/ssh_keys/test_client.py0000644000175100001710000001510700000000000021261 0ustar00runnerdockerimport pytest import mock from hcloud.ssh_keys.client import SSHKeysClient, BoundSSHKey from hcloud.ssh_keys.domain import SSHKey class TestBoundSSHKey(object): @pytest.fixture() def bound_ssh_key(self, hetzner_client): return BoundSSHKey(client=hetzner_client.ssh_keys, data=dict(id=14)) def test_bound_ssh_key_init(self, ssh_key_response): bound_ssh_key = BoundSSHKey( client=mock.MagicMock(), data=ssh_key_response["ssh_key"] ) assert bound_ssh_key.id == 2323 assert bound_ssh_key.name == "My ssh key" assert ( bound_ssh_key.fingerprint == "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f" ) assert bound_ssh_key.public_key == "ssh-rsa AAAjjk76kgf...Xt" def test_update(self, hetzner_client, bound_ssh_key, response_update_ssh_key): hetzner_client.request.return_value = response_update_ssh_key ssh_key = bound_ssh_key.update(name="New name") hetzner_client.request.assert_called_with( url="/ssh_keys/14", method="PUT", json={"name": "New name"} ) assert ssh_key.id == 2323 assert ssh_key.name == "New name" def test_delete(self, hetzner_client, bound_ssh_key, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_ssh_key.delete() hetzner_client.request.assert_called_with(url="/ssh_keys/14", method="DELETE") assert delete_success is True class TestSSHKeysClient(object): @pytest.fixture() def ssh_keys_client(self): return SSHKeysClient(client=mock.MagicMock()) def test_get_by_id(self, ssh_keys_client, ssh_key_response): ssh_keys_client._client.request.return_value = ssh_key_response ssh_key = ssh_keys_client.get_by_id(1) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys/1", method="GET" ) assert ssh_key._client is ssh_keys_client assert ssh_key.id == 2323 assert ssh_key.name == "My ssh key" @pytest.mark.parametrize( "params", [ { "name": "My ssh key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "label_selector": "k==v", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list(self, ssh_keys_client, two_ssh_keys_response, params): ssh_keys_client._client.request.return_value = two_ssh_keys_response result = ssh_keys_client.get_list(**params) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="GET", params=params ) ssh_keys = result.ssh_keys assert len(ssh_keys) == 2 ssh_keys1 = ssh_keys[0] ssh_keys2 = ssh_keys[1] assert ssh_keys1._client is ssh_keys_client assert ssh_keys1.id == 2323 assert ssh_keys1.name == "SSH-Key" assert ssh_keys2._client is ssh_keys_client assert ssh_keys2.id == 2324 assert ssh_keys2.name == "SSH-Key" @pytest.mark.parametrize( "params", [{"name": "My ssh key", "label_selector": "label1"}, {}] ) def test_get_all(self, ssh_keys_client, two_ssh_keys_response, params): ssh_keys_client._client.request.return_value = two_ssh_keys_response ssh_keys = ssh_keys_client.get_all(**params) params.update({"page": 1, "per_page": 50}) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="GET", params=params ) assert len(ssh_keys) == 2 ssh_keys1 = ssh_keys[0] ssh_keys2 = ssh_keys[1] assert ssh_keys1._client is ssh_keys_client assert ssh_keys1.id == 2323 assert ssh_keys1.name == "SSH-Key" assert ssh_keys2._client is ssh_keys_client assert ssh_keys2.id == 2324 assert ssh_keys2.name == "SSH-Key" def test_get_by_name(self, ssh_keys_client, one_ssh_keys_response): ssh_keys_client._client.request.return_value = one_ssh_keys_response ssh_keys = ssh_keys_client.get_by_name("SSH-Key") params = {"name": "SSH-Key"} ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="GET", params=params ) assert ssh_keys._client is ssh_keys_client assert ssh_keys.id == 2323 assert ssh_keys.name == "SSH-Key" def test_get_by_fingerprint(self, ssh_keys_client, one_ssh_keys_response): ssh_keys_client._client.request.return_value = one_ssh_keys_response ssh_keys = ssh_keys_client.get_by_fingerprint( "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f" ) params = {"fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f"} ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="GET", params=params ) assert ssh_keys._client is ssh_keys_client assert ssh_keys.id == 2323 assert ssh_keys.name == "SSH-Key" def test_create(self, ssh_keys_client, ssh_key_response): ssh_keys_client._client.request.return_value = ssh_key_response ssh_key = ssh_keys_client.create( name="My ssh key", public_key="ssh-rsa AAAjjk76kgf...Xt" ) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="POST", json={"name": "My ssh key", "public_key": "ssh-rsa AAAjjk76kgf...Xt"}, ) assert ssh_key.id == 2323 assert ssh_key.name == "My ssh key" @pytest.mark.parametrize( "ssh_key", [SSHKey(id=1), BoundSSHKey(mock.MagicMock(), dict(id=1))] ) def test_update(self, ssh_keys_client, ssh_key, response_update_ssh_key): ssh_keys_client._client.request.return_value = response_update_ssh_key ssh_key = ssh_keys_client.update(ssh_key, name="New name") ssh_keys_client._client.request.assert_called_with( url="/ssh_keys/1", method="PUT", json={"name": "New name"} ) assert ssh_key.id == 2323 assert ssh_key.name == "New name" @pytest.mark.parametrize( "ssh_key", [SSHKey(id=1), BoundSSHKey(mock.MagicMock(), dict(id=1))] ) def test_delete(self, ssh_keys_client, ssh_key, generic_action): ssh_keys_client._client.request.return_value = generic_action delete_success = ssh_keys_client.delete(ssh_key) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys/1", method="DELETE" ) assert delete_success is True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/ssh_keys/test_domain.py0000644000175100001710000000053000000000000021244 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.ssh_keys.domain import SSHKey class TestSSHKey(object): def test_created_is_datetime(self): sshKey = SSHKey(id=1, created="2016-01-30T23:50+00:00") assert sshKey.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/test_hcloud.py0000644000175100001710000001603200000000000017427 0ustar00runnerdocker#!/usr/bin/env python # -*- coding: utf-8 -*- import json from unittest.mock import MagicMock import requests import pytest from hcloud import Client, APIException class TestHetznerClient(object): @pytest.fixture() def client(self): Client._version = "0.0.0" client = Client(token="project_token") client._requests_session = MagicMock() return client @pytest.fixture() def response(self): response = requests.Response() response.status_code = 200 response._content = json.dumps({"result": "data"}).encode("utf-8") return response @pytest.fixture() def fail_response(self, response): response.status_code = 422 error = { "code": "invalid_input", "message": "invalid input in field 'broken_field': is too long", "details": { "fields": [{"name": "broken_field", "messages": ["is too long"]}] }, } response._content = json.dumps({"error": error}).encode("utf-8") return response @pytest.fixture() def rate_limit_response(self, response): response.status_code = 422 error = { "code": "rate_limit_exceeded", "message": "limit of 10 requests per hour reached", "details": {}, } response._content = json.dumps({"error": error}).encode("utf-8") return response def test__get_user_agent(self, client): user_agent = client._get_user_agent() assert user_agent == "hcloud-python/0.0.0" def test__get_user_agent_with_application_name(self, client): client = Client(token="project_token", application_name="my-app") user_agent = client._get_user_agent() assert user_agent == "my-app hcloud-python/0.0.0" def test__get_user_agent_with_application_name_and_version(self, client): client = Client( token="project_token", application_name="my-app", application_version="1.0.0", ) user_agent = client._get_user_agent() assert user_agent == "my-app/1.0.0 hcloud-python/0.0.0" def test__get_headers(self, client): headers = client._get_headers() assert headers == { "User-Agent": "hcloud-python/0.0.0", "Authorization": "Bearer project_token", } def test_request_library_mocked(self, client): response = client.request("POST", "url", params={"1": 2}) assert response.__class__.__name__ == "MagicMock" def test_request_ok(self, client, response): client._requests_session.request.return_value = response response = client.request( "POST", "/servers", params={"argument": "value"}, timeout=2 ) client._requests_session.request.assert_called_once() assert client._requests_session.request.call_args[0] == ( "POST", "https://api.hetzner.cloud/v1/servers", ) assert client._requests_session.request.call_args[1]["params"] == { "argument": "value" } assert client._requests_session.request.call_args[1]["timeout"] == 2 assert response == {"result": "data"} def test_request_fails(self, client, fail_response): client._requests_session.request.return_value = fail_response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert error.code == "invalid_input" assert error.message == "invalid input in field 'broken_field': is too long" assert error.details["fields"][0]["name"] == "broken_field" def test_request_500(self, client, fail_response): fail_response.status_code = 500 fail_response.reason = "Internal Server Error" fail_response._content = "Internal Server Error" client._requests_session.request.return_value = fail_response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert error.code == 500 assert error.message == "Internal Server Error" assert error.details["content"] == "Internal Server Error" def test_request_broken_json_200(self, client, response): content = "{'key': 'value'".encode("utf-8") response.reason = "OK" response._content = content client._requests_session.request.return_value = response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert error.code == 200 assert error.message == "OK" assert error.details["content"] == content def test_request_empty_content_200(self, client, response): content = "" response.reason = "OK" response._content = content client._requests_session.request.return_value = response response = client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) assert response == "" def test_request_500_empty_content(self, client, fail_response): fail_response.status_code = 500 fail_response.reason = "Internal Server Error" fail_response._content = "" client._requests_session.request.return_value = fail_response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert error.code == 500 assert error.message == "Internal Server Error" assert error.details["content"] == "" assert str(error) == "Internal Server Error" def test_request_limit(self, client, rate_limit_response): client._retry_wait_time = 0 client._requests_session.request.return_value = rate_limit_response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert client._requests_session.request.call_count == 5 assert error.code == "rate_limit_exceeded" assert error.message == "limit of 10 requests per hour reached" def test_request_limit_then_success(self, client, rate_limit_response): client._retry_wait_time = 0 response = requests.Response() response.status_code = 200 response._content = json.dumps({"result": "data"}).encode("utf-8") client._requests_session.request.side_effect = [rate_limit_response, response] client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) assert client._requests_session.request.call_count == 2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1629203244.8545113 hcloud-1.16.0/tests/unit/volumes/0000755000175100001710000000000000000000000016230 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/volumes/__init__.py0000644000175100001710000000000000000000000020327 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/volumes/conftest.py0000644000175100001710000001400100000000000020423 0ustar00runnerdockerimport pytest @pytest.fixture() def volume_response(): return { "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "database-storage", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", } } @pytest.fixture() def two_volumes_response(): return { "volumes": [ { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "database-storage", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", }, { "id": 2, "created": "2016-01-30T23:50:11+00:00", "name": "vault-storage", "server": 10, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 2", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", }, ] } @pytest.fixture() def one_volumes_response(): return { "volumes": [ { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "database-storage", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", } ] } @pytest.fixture() def volume_create_response(): return { "volume": { "id": 4711, "created": "2016-01-30T23:50:11+00:00", "name": "database-storage", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", }, "action": { "id": 13, "command": "create_volume", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, "next_actions": [ { "id": 13, "command": "start_server", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } ], } @pytest.fixture() def response_update_volume(): return { "volume": { "id": 4711, "created": "2016-01-30T23:50:11+00:00", "name": "new-name", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "format": "xfs", "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "labels": {}, "status": "available", } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "attach_volume", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/volumes/test_client.py0000644000175100001710000003517100000000000021126 0ustar00runnerdockerimport pytest from dateutil.parser import isoparse import mock from hcloud.actions.client import BoundAction from hcloud.servers.client import BoundServer from hcloud.servers.domain import Server from hcloud.volumes.client import VolumesClient, BoundVolume from hcloud.volumes.domain import Volume from hcloud.locations.client import BoundLocation from hcloud.locations.domain import Location class TestBoundVolume(object): @pytest.fixture() def bound_volume(self, hetzner_client): return BoundVolume(client=hetzner_client.volumes, data=dict(id=14)) def test_bound_volume_init(self, volume_response): bound_volume = BoundVolume( client=mock.MagicMock(), data=volume_response["volume"] ) assert bound_volume.id == 1 assert bound_volume.created == isoparse("2016-01-30T23:50:11+00:00") assert bound_volume.name == "database-storage" assert isinstance(bound_volume.server, BoundServer) assert bound_volume.server.id == 12 assert bound_volume.size == 42 assert bound_volume.linux_device == "/dev/disk/by-id/scsi-0HC_Volume_4711" assert bound_volume.protection == {"delete": False} assert bound_volume.labels == {} assert bound_volume.status == "available" assert isinstance(bound_volume.location, BoundLocation) assert bound_volume.location.id == 1 assert bound_volume.location.name == "fsn1" assert bound_volume.location.description == "Falkenstein DC Park 1" assert bound_volume.location.country == "DE" assert bound_volume.location.city == "Falkenstein" assert bound_volume.location.latitude == 50.47612 assert bound_volume.location.longitude == 12.370071 def test_get_actions(self, hetzner_client, bound_volume, response_get_actions): hetzner_client.request.return_value = response_get_actions actions = bound_volume.get_actions(sort="id") hetzner_client.request.assert_called_with( url="/volumes/14/actions", method="GET", params={"page": 1, "per_page": 50, "sort": "id"}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0].id == 13 assert actions[0].command == "attach_volume" def test_update(self, hetzner_client, bound_volume, response_update_volume): hetzner_client.request.return_value = response_update_volume volume = bound_volume.update(name="new-name") hetzner_client.request.assert_called_with( url="/volumes/14", method="PUT", json={"name": "new-name"} ) assert volume.id == 4711 assert volume.name == "new-name" def test_delete(self, hetzner_client, bound_volume, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_volume.delete() hetzner_client.request.assert_called_with(url="/volumes/14", method="DELETE") assert delete_success is True def test_change_protection(self, hetzner_client, bound_volume, generic_action): hetzner_client.request.return_value = generic_action action = bound_volume.change_protection(True) hetzner_client.request.assert_called_with( url="/volumes/14/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", (Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))) ) def test_attach(self, hetzner_client, bound_volume, server, generic_action): hetzner_client.request.return_value = generic_action action = bound_volume.attach(server) hetzner_client.request.assert_called_with( url="/volumes/14/actions/attach", method="POST", json={"server": 1} ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", (Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))) ) def test_attach_with_automount( self, hetzner_client, bound_volume, server, generic_action ): hetzner_client.request.return_value = generic_action action = bound_volume.attach(server, False) hetzner_client.request.assert_called_with( url="/volumes/14/actions/attach", method="POST", json={"server": 1, "automount": False}, ) assert action.id == 1 assert action.progress == 0 def test_detach(self, hetzner_client, bound_volume, generic_action): hetzner_client.request.return_value = generic_action action = bound_volume.detach() hetzner_client.request.assert_called_with( url="/volumes/14/actions/detach", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_resize(self, hetzner_client, bound_volume, generic_action): hetzner_client.request.return_value = generic_action action = bound_volume.resize(50) hetzner_client.request.assert_called_with( url="/volumes/14/actions/resize", method="POST", json={"size": 50} ) assert action.id == 1 assert action.progress == 0 class TestVolumesClient(object): @pytest.fixture() def volumes_client(self): return VolumesClient(client=mock.MagicMock()) def test_get_by_id(self, volumes_client, volume_response): volumes_client._client.request.return_value = volume_response bound_volume = volumes_client.get_by_id(1) volumes_client._client.request.assert_called_with( url="/volumes/1", method="GET" ) assert bound_volume._client is volumes_client assert bound_volume.id == 1 assert bound_volume.name == "database-storage" @pytest.mark.parametrize( "params", [{"label_selector": "label1", "page": 1, "per_page": 10}, {"name": ""}, {}], ) def test_get_list(self, volumes_client, two_volumes_response, params): volumes_client._client.request.return_value = two_volumes_response result = volumes_client.get_list(**params) volumes_client._client.request.assert_called_with( url="/volumes", method="GET", params=params ) bound_volumes = result.volumes assert result.meta is None assert len(bound_volumes) == 2 bound_volume1 = bound_volumes[0] bound_volume2 = bound_volumes[1] assert bound_volume1._client is volumes_client assert bound_volume1.id == 1 assert bound_volume1.name == "database-storage" assert bound_volume2._client is volumes_client assert bound_volume2.id == 2 assert bound_volume2.name == "vault-storage" @pytest.mark.parametrize("params", [{"label_selector": "label1"}]) def test_get_all(self, volumes_client, two_volumes_response, params): volumes_client._client.request.return_value = two_volumes_response bound_volumes = volumes_client.get_all(**params) params.update({"page": 1, "per_page": 50}) volumes_client._client.request.assert_called_with( url="/volumes", method="GET", params=params ) assert len(bound_volumes) == 2 bound_volume1 = bound_volumes[0] bound_volume2 = bound_volumes[1] assert bound_volume1._client is volumes_client assert bound_volume1.id == 1 assert bound_volume1.name == "database-storage" assert bound_volume2._client is volumes_client assert bound_volume2.id == 2 assert bound_volume2.name == "vault-storage" def test_get_by_name(self, volumes_client, one_volumes_response): volumes_client._client.request.return_value = one_volumes_response bound_volume = volumes_client.get_by_name("database-storage") params = {"name": "database-storage"} volumes_client._client.request.assert_called_with( url="/volumes", method="GET", params=params ) assert bound_volume._client is volumes_client assert bound_volume.id == 1 assert bound_volume.name == "database-storage" def test_create_with_location(self, volumes_client, volume_create_response): volumes_client._client.request.return_value = volume_create_response response = volumes_client.create( 100, "database-storage", location=Location(name="location"), automount=False, format="xfs", ) volumes_client._client.request.assert_called_with( url="/volumes", method="POST", json={ "name": "database-storage", "size": 100, "location": "location", "automount": False, "format": "xfs", }, ) bound_volume = response.volume action = response.action next_actions = response.next_actions assert bound_volume._client is volumes_client assert bound_volume.id == 4711 assert bound_volume.name == "database-storage" assert action.id == 13 assert next_actions[0].command == "start_server" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_create_with_server(self, volumes_client, server, volume_create_response): volumes_client._client.request.return_value = volume_create_response volumes_client.create( 100, "database-storage", server=server, automount=False, format="xfs" ) volumes_client._client.request.assert_called_with( url="/volumes", method="POST", json={ "name": "database-storage", "size": 100, "server": 1, "automount": False, "format": "xfs", }, ) def test_create_negative_size(self, volumes_client): with pytest.raises(ValueError) as e: volumes_client.create( -100, "database-storage", location=Location(name="location") ) assert str(e.value) == "size must be greater than 0" volumes_client._client.request.assert_not_called() @pytest.mark.parametrize( "location,server", [(None, None), ("location", Server(id=1))] ) def test_create_wrong_location_server_combination( self, volumes_client, location, server ): with pytest.raises(ValueError) as e: volumes_client.create( 100, "database-storage", location=location, server=server ) assert str(e.value) == "only one of server or location must be provided" volumes_client._client.request.assert_not_called() @pytest.mark.parametrize( "volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))] ) def test_get_actions_list(self, volumes_client, volume, response_get_actions): volumes_client._client.request.return_value = response_get_actions result = volumes_client.get_actions_list(volume, sort="id") volumes_client._client.request.assert_called_with( url="/volumes/1/actions", method="GET", params={"sort": "id"} ) actions = result.actions assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == volumes_client._client.actions assert actions[0].id == 13 assert actions[0].command == "attach_volume" @pytest.mark.parametrize( "volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))] ) def test_update(self, volumes_client, volume, response_update_volume): volumes_client._client.request.return_value = response_update_volume volume = volumes_client.update(volume, name="new-name") volumes_client._client.request.assert_called_with( url="/volumes/1", method="PUT", json={"name": "new-name"} ) assert volume.id == 4711 assert volume.name == "new-name" @pytest.mark.parametrize( "volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))] ) def test_change_protection(self, volumes_client, volume, generic_action): volumes_client._client.request.return_value = generic_action action = volumes_client.change_protection(volume, True) volumes_client._client.request.assert_called_with( url="/volumes/1/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))] ) def test_delete(self, volumes_client, volume, generic_action): volumes_client._client.request.return_value = generic_action delete_success = volumes_client.delete(volume) volumes_client._client.request.assert_called_with( url="/volumes/1", method="DELETE" ) assert delete_success is True @pytest.mark.parametrize( "server,volume", [ (Server(id=1), Volume(id=12)), ( BoundServer(mock.MagicMock(), dict(id=1)), BoundVolume(mock.MagicMock(), dict(id=12)), ), ], ) def test_attach(self, volumes_client, server, volume, generic_action): volumes_client._client.request.return_value = generic_action action = volumes_client.attach(volume, server) volumes_client._client.request.assert_called_with( url="/volumes/12/actions/attach", method="POST", json={"server": 1} ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "volume", [Volume(id=12), BoundVolume(mock.MagicMock(), dict(id=12))] ) def test_detach(self, volumes_client, volume, generic_action): volumes_client._client.request.return_value = generic_action action = volumes_client.detach(volume) volumes_client._client.request.assert_called_with( url="/volumes/12/actions/detach", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "volume", [Volume(id=12), BoundVolume(mock.MagicMock(), dict(id=12))] ) def test_resize(self, volumes_client, volume, generic_action): volumes_client._client.request.return_value = generic_action action = volumes_client.resize(volume, 50) volumes_client._client.request.assert_called_with( url="/volumes/12/actions/resize", method="POST", json={"size": 50} ) assert action.id == 1 assert action.progress == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629203235.0 hcloud-1.16.0/tests/unit/volumes/test_domain.py0000644000175100001710000000052700000000000021114 0ustar00runnerdockerimport datetime from dateutil.tz import tzoffset from hcloud.volumes.domain import Volume class TestVolume(object): def test_created_is_datetime(self): volume = Volume(id=1, created="2016-01-30T23:50+00:00") assert volume.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) )