feat: vendor hcloud python dependency (#244)

* chore: ignore venv directories

* chore: ignore integration test generated inventory

* feat: vendor hcloud package

* import https://github.com/hetznercloud/hcloud-python

* use vendored hcloud in modules

* update integration test requirements

* make vendor script self contained

* chore: add  check-hcloud-vendor pre-commit hook

* pin hcloud version to v.1.24.0

* move vendored __version__.py file to _version.py

* update comment about galaxy-importer filename lint
This commit is contained in:
Jonas L 2023-07-11 11:15:08 +02:00
parent bf9700ab23
commit 8c7cd1e8f9
84 changed files with 8433 additions and 79 deletions

4
.gitignore vendored
View file

@ -1,3 +1,5 @@
.venv
venv
# Created by https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv # Created by https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv
# Edit at https://www.gitignore.io/?templates=git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv # Edit at https://www.gitignore.io/?templates=git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv
@ -55,7 +57,6 @@ flycheck_*.el
# network security # network security
/network-security.data /network-security.data
### Git ### ### Git ###
# Created by git for backups. To disable backups in Git: # Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false # $ git config --global mergetool.keepBackup false
@ -387,3 +388,4 @@ $RECYCLE.BIN/
# End of https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv # End of https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv
cloud-config-hcloud.ini cloud-config-hcloud.ini
tests/integration/inventory

View file

@ -1,6 +1,7 @@
--- ---
# See https://pre-commit.com for more information # See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
exclude: ^plugins/module_utils/vendor/hcloud/.*$
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.4.0
@ -52,3 +53,11 @@ repos:
entry: antsibull-changelog lint entry: antsibull-changelog lint
pass_filenames: false pass_filenames: false
files: ^changelogs/.*$ files: ^changelogs/.*$
- id: check-hcloud-vendor
name: check hcloud vendor
description: Ensure the hcloud vendored files are in sync
language: python
entry: python3 scripts/vendor.py
pass_filenames: false
files: ^scripts/vendor.py$

9
Makefile Normal file
View file

@ -0,0 +1,9 @@
SHELL := bash
.PHONY: vendor clean
vendor:
python3 scripts/vendor.py
clean:
git clean -xdf \
-e tests/integration/cloud-config-hcloud.ini

View file

@ -0,0 +1,12 @@
release_summary: |
This release bundles the hcloud dependency in the collection, this allows us to ship
new features or bug fixes without having to release new major versions and require the
users to upgrade their version of the hcloud dependency.
minor_changes:
- Bundle hcloud python dependency inside the collection.
- >
python-dateutil >= 2.7.5 is now required by the collection. If you already have the
hcloud package installed, this dependency should also be installed.
- >
requests >= 2.20 is now required by the collection. If you already have the hcloud
package installed, this dependency should also be installed.

View file

@ -17,7 +17,8 @@ options:
default: https://api.hetzner.cloud/v1 default: https://api.hetzner.cloud/v1
type: str type: str
requirements: requirements:
- hcloud-python >= 1.20.0 - python-dateutil >= 2.7.5
- requests >=2.20
seealso: seealso:
- name: Documentation for Hetzner Cloud API - name: Documentation for Hetzner Cloud API
description: Complete reference for the Hetzner Cloud API. description: Complete reference for the Hetzner Cloud API.

View file

@ -121,13 +121,11 @@ from ansible.errors import AnsibleError
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.release import __version__ from ansible.release import __version__
from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import (
try: HAS_DATEUTIL,
from hcloud import APIException, hcloud HAS_REQUESTS,
)
HAS_HCLOUD = True from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor import hcloud
except ImportError:
HAS_HCLOUD = False
class InventoryModule(BaseInventoryPlugin, Constructable): class InventoryModule(BaseInventoryPlugin, Constructable):
@ -159,7 +157,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
# We test the API Token against the location API, because this is the API with the smallest result # We test the API Token against the location API, because this is the API with the smallest result
# and not controllable from the customer. # and not controllable from the customer.
self.client.locations.get_all() self.client.locations.get_all()
except APIException: except hcloud.APIException:
raise AnsibleError("Invalid Hetzner Cloud API Token.") raise AnsibleError("Invalid Hetzner Cloud API Token.")
def _get_servers(self): def _get_servers(self):
@ -177,7 +175,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
self.network = self.client.networks.get_by_name(network) self.network = self.client.networks.get_by_name(network)
if self.network is None: if self.network is None:
self.network = self.client.networks.get_by_id(network) self.network = self.client.networks.get_by_id(network)
except APIException: except hcloud.APIException:
raise AnsibleError("The given network is not found.") raise AnsibleError("The given network is not found.")
tmp = [] tmp = []
@ -322,8 +320,10 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
def parse(self, inventory, loader, path, cache=True): def parse(self, inventory, loader, path, cache=True):
super().parse(inventory, loader, path, cache) super().parse(inventory, loader, path, cache)
if not HAS_HCLOUD: if not HAS_REQUESTS:
raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires hcloud-python.") raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires requests.")
if not HAS_DATEUTIL:
raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires python-dateutil.")
self._read_config_data(path) self._read_config_data(path)
self._configure_hcloud_client() self._configure_hcloud_client()

View file

@ -5,13 +5,20 @@
from ansible.module_utils.ansible_release import __version__ from ansible.module_utils.ansible_release import __version__
from ansible.module_utils.basic import env_fallback, missing_required_lib from ansible.module_utils.basic import env_fallback, missing_required_lib
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor import hcloud
HAS_REQUESTS = True
HAS_DATEUTIL = True
try: try:
import hcloud import requests # pylint: disable=unused-import
HAS_HCLOUD = True
except ImportError: except ImportError:
HAS_HCLOUD = False HAS_REQUESTS = False
try:
import dateutil # pylint: disable=unused-import
except ImportError:
HAS_DATEUTIL = False
class Hcloud: class Hcloud:
@ -19,8 +26,10 @@ class Hcloud:
self.module = module self.module = module
self.represent = represent self.represent = represent
self.result = {"changed": False, self.represent: None} self.result = {"changed": False, self.represent: None}
if not HAS_HCLOUD: if not HAS_REQUESTS:
module.fail_json(msg=missing_required_lib("hcloud-python")) module.fail_json(msg=missing_required_lib("requests"))
if not HAS_DATEUTIL:
module.fail_json(msg=missing_required_lib("python-dateutil"))
self._build_client() self._build_client()
def _build_client(self): def _build_client(self):

View file

View file

@ -0,0 +1,2 @@
from ._exceptions import APIException, HCloudException # noqa
from .hcloud import Client # noqa

View file

@ -0,0 +1,14 @@
class HCloudException(Exception):
"""There was an error while using the hcloud library"""
class APIException(HCloudException):
"""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

View file

@ -0,0 +1 @@
VERSION = "1.24.0" # x-release-please-version

View file

View file

@ -0,0 +1,90 @@
import time
from ..core.client import BoundModelBase, ClientEntityBase
from .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 <hcloud.actions.client.BoundAction>`
"""
response = self._client.request(url=f"/actions/{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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 <hcloud.actions.client.BoundAction>`]
"""
return super().get_all(status=status, sort=sort)

View file

@ -0,0 +1,75 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from .._exceptions import HCloudException
from ..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 ActionException(HCloudException):
"""A generic action exception"""
def __init__(self, action):
self.action = action
class ActionFailedException(ActionException):
"""The Action you were waiting for failed"""
class ActionTimeoutException(ActionException):
"""The Action you were waiting for timed out"""

View file

@ -0,0 +1,316 @@
from ..actions.client import BoundAction
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from .domain import (
Certificate,
CreateManagedCertificateResponse,
ManagedCertificateError,
ManagedCertificateStatus,
)
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().__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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 <hcloud.actions.client.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 <hcloud.certificates.client.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 <hcloud.certificates.client.BoundCertificate>`
"""
response = self._client.request(url=f"/certificates/{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 <hcloud.certificates.client.BoundCertificate>`], :class:`Meta <hcloud.core.domain.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], 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 <hcloud.certificates.client.BoundCertificate>`]
"""
return super().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 <hcloud.certificates.client.BoundCertificate>`
"""
return super().get_by_name(name)
def create(self, name, certificate, private_key, labels=None):
# type: (str, 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 <hcloud.certificates.client.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 <hcloud.certificates.client.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 <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>`
:param name: str (optional)
New name to set
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`
"""
data = {}
if name is not None:
data["name"] = name
if labels is not None:
data["labels"] = labels
response = self._client.request(
url=f"/certificates/{certificate.id}",
method="PUT",
json=data,
)
return BoundCertificate(self, response["certificate"])
def delete(self, certificate):
# type: (Certificate) -> bool
self._client.request(
url=f"/certificates/{certificate.id}",
method="DELETE",
)
"""Deletes a certificate.
:param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.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 <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.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 <hcloud.actions.client.BoundAction>`]
"""
return super().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 <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>`
:return: :class:`BoundAction <hcloud.actions.client.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"])

View file

@ -0,0 +1,120 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 <hcloud.certificate.client.BoundCertificate>`
The created server
:param action: :class:`BoundAction <hcloud.actions.client.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

View file

View file

@ -0,0 +1,127 @@
from .domain import add_meta_to_result
class ClientEntityBase:
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:
"""
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:
"""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

View file

@ -0,0 +1,75 @@
from collections import namedtuple
class BaseDomain:
__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:
__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 = f"PageResults{attr_name.capitalize()}"
PageResults = namedtuple(class_name, [attr_name, "meta"])
return PageResults(**{attr_name: result, "meta": Meta.parse_meta(json_content)})

View file

@ -0,0 +1,111 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..locations.client import BoundLocation
from ..server_types.client import BoundServerType
from .domain import Datacenter, DatacenterServerTypes
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().__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 <hcloud.datacenters.client.BoundDatacenter>`
"""
response = self._client.request(url=f"/datacenters/{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 <hcloud.datacenters.client.BoundDatacenter>`], :class:`Meta <hcloud.core.domain.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 <hcloud.datacenters.client.BoundDatacenter>`]
"""
return super().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 <hcloud.datacenters.client.BoundDatacenter>`
"""
return super().get_by_name(name)

View file

@ -0,0 +1,42 @@
from ..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 <hcloud.locations.client.BoundLocation>`
:param server_types: :class:`DatacenterServerTypes <hcloud.datacenters.domain.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 <hcloud.server_types.client.BoundServerTypes>`]
All available server types for this datacenter
:param supported: List[:class:`BoundServerTypes <hcloud.server_types.client.BoundServerTypes>`]
All supported server types for this datacenter
:param available_for_migration: List[:class:`BoundServerTypes <hcloud.server_types.client.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

View file

@ -0,0 +1,34 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..core.domain import BaseDomain
class DeprecationInfo(BaseDomain):
"""Describes if, when & how the resources was deprecated. If this field is set to ``None`` the resource is not
deprecated. If it has a value, it is considered deprecated.
:param announced: datetime
Date of when the deprecation was announced.
:param unavailable_after: datetime
After the time in this field, the resource will not be available from the general listing endpoint of the
resource type, and it can not be used in new resources. For example, if this is an image, you can not create
new servers with this image after the mentioned date.
"""
__slots__ = (
"announced",
"unavailable_after",
)
def __init__(
self,
announced=None,
unavailable_after=None,
):
self.announced = isoparse(announced) if announced else None
self.unavailable_after = (
isoparse(unavailable_after) if unavailable_after else None
)

View file

@ -0,0 +1,416 @@
from ..actions.client import BoundAction
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from .domain import (
CreateFirewallResponse,
Firewall,
FirewallResource,
FirewallResourceLabelSelector,
FirewallRule,
)
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 ..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().__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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 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 <hcloud.actions.client.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 <hcloud.firewalls.client.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 <hcloud.firewalls.domain.FirewallRule>`]
:return: List[:class:`BoundAction <hcloud.actions.client.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 <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.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 <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.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 <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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=f"/firewalls/{firewall.id}/actions",
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 <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.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 <hcloud.actions.client.BoundAction>`]
"""
return super().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 <hcloud.firewalls.client.BoundFirewall>`
"""
response = self._client.request(url=f"/firewalls/{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 <hcloud.firewalls.client.BoundFirewall>`], :class:`Meta <hcloud.core.domain.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 <hcloud.firewalls.client.BoundFirewall>`]
"""
return super().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 <hcloud.firewalls.client.BoundFirewall>`
"""
return super().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 <hcloud.firewalls.domain.FirewallRule>`] (optional)
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] (optional)
:return: :class:`CreateFirewallResponse <hcloud.firewalls.domain.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 <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:param name: str (optional)
New name to set
:return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`
"""
data = {}
if labels is not None:
data["labels"] = labels
if name is not None:
data["name"] = name
response = self._client.request(
url=f"/firewalls/{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 <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:return: boolean
"""
self._client.request(
url=f"/firewalls/{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 <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`]
:return: List[:class:`BoundAction <hcloud.actions.client.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 <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.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 <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.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"]]

View file

@ -0,0 +1,180 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 <hcloud.firewalls.domain.FirewallRule>`]
Rules of the Firewall
:param applied_to: List[:class:`FirewallResource <hcloud.firewalls.domain.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 <hcloud.firewalls.client.BoundFirewall>`
The Firewall which was created
:param actions: List[:class:`BoundAction <hcloud.actions.client.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

View file

@ -0,0 +1,422 @@
from ..actions.client import BoundAction
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from ..locations.client import BoundLocation
from .domain import CreateFloatingIPResponse, FloatingIP
class BoundFloatingIP(BoundModelBase):
model = FloatingIP
def __init__(self, client, data, complete=True):
from ..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().__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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 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 <hcloud.actions.client.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 <hcloud.floating_ips.client.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 <hcloud.actions.client.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 <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>`
Server the Floating IP shall be assigned to
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.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 <hcloud.actions.client.BoundAction>`]
"""
return super().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 <hcloud.floating_ips.client.BoundFloatingIP>`
"""
response = self._client.request(url=f"/floating_ips/{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 <hcloud.floating_ips.client.BoundFloatingIP>`], :class:`Meta <hcloud.core.domain.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 <hcloud.floating_ips.client.BoundFloatingIP>`]
"""
return super().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 <hcloud.floating_ips.client.BoundFloatingIP>`
"""
return super().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 <hcloud.locations.client.BoundLocation>` or :class:`Location <hcloud.locations.domain.Location>` (
Home location (routing is optimized for that location). Only optional if server argument is passed.
:param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>`
Server to assign the Floating IP to
:param name: str (optional)
:return: :class:`CreateFloatingIPResponse <hcloud.floating_ips.domain.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 <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.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 <hcloud.floating_ips.client.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=f"/floating_ips/{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 <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
:return: boolean
"""
self._client.request(
url=f"/floating_ips/{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 <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
:param delete: boolean
If true, prevents the Floating IP from being deleted
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
:param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>`
Server the Floating IP shall be assigned to
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.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 <hcloud.actions.client.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"])

View file

@ -0,0 +1,99 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 <hcloud.servers.client.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 <hcloud.locations.client.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 <hcloud.floating_ips.client.BoundFloatingIP>`
The Floating IP which was created
:param action: :class:`BoundAction <hcloud.actions.client.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

View file

@ -0,0 +1,225 @@
import time
from typing import Optional, Union
try:
import requests
except ImportError:
requests = None
from ._version import VERSION
from ._exceptions import APIException
from .actions.client import ActionsClient
from .certificates.client import CertificatesClient
from .datacenters.client import DatacentersClient
from .firewalls.client import FirewallsClient
from .floating_ips.client import FloatingIPsClient
from .images.client import ImagesClient
from .isos.client import IsosClient
from .load_balancer_types.client import LoadBalancerTypesClient
from .load_balancers.client import LoadBalancersClient
from .locations.client import LocationsClient
from .networks.client import NetworksClient
from .placement_groups.client import PlacementGroupsClient
from .primary_ips.client import PrimaryIPsClient
from .server_types.client import ServerTypesClient
from .servers.client import ServersClient
from .ssh_keys.client import SSHKeysClient
from .volumes.client import VolumesClient
class Client:
"""Base Client for accessing the Hetzner Cloud API"""
_version = VERSION
_retry_wait_time = 0.5
__user_agent_prefix = "hcloud-python"
def __init__(
self,
token: str,
api_endpoint: str = "https://api.hetzner.cloud/v1",
application_name: Optional[str] = None,
application_version: Optional[str] = None,
poll_interval: int = 1,
):
"""Create an new Client instance
:param token: Hetzner Cloud API token
:param api_endpoint: Hetzner Cloud API endpoint
:param application_name: Your application name
:param application_version: Your application _version
:param poll_interval: Interval for polling information from Hetzner Cloud API in seconds
"""
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 <hcloud.datacenters.client.DatacentersClient>`
"""
self.locations = LocationsClient(self)
"""LocationsClient Instance
:type: :class:`LocationsClient <hcloud.locations.client.LocationsClient>`
"""
self.servers = ServersClient(self)
"""ServersClient Instance
:type: :class:`ServersClient <hcloud.servers.client.ServersClient>`
"""
self.server_types = ServerTypesClient(self)
"""ServerTypesClient Instance
:type: :class:`ServerTypesClient <hcloud.server_types.client.ServerTypesClient>`
"""
self.volumes = VolumesClient(self)
"""VolumesClient Instance
:type: :class:`VolumesClient <hcloud.volumes.client.VolumesClient>`
"""
self.actions = ActionsClient(self)
"""ActionsClient Instance
:type: :class:`ActionsClient <hcloud.actions.client.ActionsClient>`
"""
self.images = ImagesClient(self)
"""ImagesClient Instance
:type: :class:`ImagesClient <hcloud.images.client.ImagesClient>`
"""
self.isos = IsosClient(self)
"""ImagesClient Instance
:type: :class:`IsosClient <hcloud.isos.client.IsosClient>`
"""
self.ssh_keys = SSHKeysClient(self)
"""SSHKeysClient Instance
:type: :class:`SSHKeysClient <hcloud.ssh_keys.client.SSHKeysClient>`
"""
self.floating_ips = FloatingIPsClient(self)
"""FloatingIPsClient Instance
:type: :class:`FloatingIPsClient <hcloud.floating_ips.client.FloatingIPsClient>`
"""
self.primary_ips = PrimaryIPsClient(self)
"""PrimaryIPsClient Instance
:type: :class:`PrimaryIPsClient <hcloud.primary_ips.client.PrimaryIPsClient>`
"""
self.networks = NetworksClient(self)
"""NetworksClient Instance
:type: :class:`NetworksClient <hcloud.networks.client.NetworksClient>`
"""
self.certificates = CertificatesClient(self)
"""CertificatesClient Instance
:type: :class:`CertificatesClient <hcloud.certificates.client.CertificatesClient>`
"""
self.load_balancers = LoadBalancersClient(self)
"""LoadBalancersClient Instance
:type: :class:`LoadBalancersClient <hcloud.load_balancers.client.LoadBalancersClient>`
"""
self.load_balancer_types = LoadBalancerTypesClient(self)
"""LoadBalancerTypesClient Instance
:type: :class:`LoadBalancerTypesClient <hcloud.load_balancer_types.client.LoadBalancerTypesClient>`
"""
self.firewalls = FirewallsClient(self)
"""FirewallsClient Instance
:type: :class:`FirewallsClient <hcloud.firewalls.client.FirewallsClient>`
"""
self.placement_groups = PlacementGroupsClient(self)
"""PlacementGroupsClient Instance
:type: :class:`PlacementGroupsClient <hcloud.placement_groups.client.PlacementGroupsClient>`
"""
def _get_user_agent(self) -> str:
"""Get the user agent of the hcloud-python instance with the user application name (if specified)
:return: The user agent of this hcloud-python instance
"""
user_agents = []
for name, version in [
(self._application_name, self._application_version),
(self.__user_agent_prefix, self._version),
]:
if name is not None:
user_agents.append(name if version is None else f"{name}/{version}")
return " ".join(user_agents)
def _get_headers(self) -> dict:
headers = {
"User-Agent": self._get_user_agent(),
"Authorization": f"Bearer {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_content(self, content: dict):
raise APIException(
code=content["error"]["code"],
message=content["error"]["message"],
details=content["error"]["details"],
)
def request(
self,
method: str,
url: str,
tries: int = 1,
**kwargs,
) -> Union[bytes, dict]:
"""Perform a request to the Hetzner Cloud API, wrapper around requests.request
:param method: HTTP Method to perform the Request
:param url: URL of the Endpoint
:param tries: Tries of the request (used internally, should not be set by the user)
:return: Response
"""
response = self._requests_session.request(
method=method,
url=self._api_endpoint + url,
headers=self._get_headers(),
**kwargs,
)
content = response.content
try:
if len(content) > 0:
content = response.json()
except (TypeError, ValueError):
self._raise_exception_from_response(response)
if not response.ok:
if content:
if 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_content(content)
else:
self._raise_exception_from_response(response)
return content

View file

View file

@ -0,0 +1,39 @@
import re
from typing import Dict
class LabelValidator:
KEY_REGEX = re.compile(
r"^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,61}[a-z0-9A-Z])?$"
)
VALUE_REGEX = re.compile(
r"^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,61})?[a-z0-9A-Z]$|$)"
)
@staticmethod
def validate(labels: Dict[str, str]) -> bool:
"""Validates Labels. If you want to know which key/value pair of the dict is not correctly formatted
use :func:`~hcloud.helpers.labels.validate_verbose`.
:return: bool
"""
for k, v in labels.items():
if LabelValidator.KEY_REGEX.match(k) is None:
return False
if LabelValidator.VALUE_REGEX.match(v) is None:
return False
return True
@staticmethod
def validate_verbose(labels: Dict[str, str]) -> (bool, str):
"""Validates Labels and returns the corresponding error message if something is wrong. Returns True, <empty string>
if everything is fine.
:return: bool, str
"""
for k, v in labels.items():
if LabelValidator.KEY_REGEX.match(k) is None:
return False, f"label key {k} is not correctly formatted"
if LabelValidator.VALUE_REGEX.match(v) is None:
return False, f"label value {v} (key: {k}) is not correctly formatted"
return True, ""

View file

View file

@ -0,0 +1,354 @@
from ..actions.client import BoundAction
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from .domain import Image
class BoundImage(BoundModelBase):
model = Image
def __init__(self, client, data):
from ..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().__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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 <hcloud.actions.client.BoundAction>`]
"""
return self._client.get_actions(self, status=status, sort=sort)
def update(self, description=None, type=None, labels=None):
# type: (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 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 <hcloud.images.client.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 <hcloud.actions.client.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 <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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=f"/images/{image.id}/actions",
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 <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.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 <hcloud.actions.client.BoundAction>`]
"""
return super().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 <hcloud.images.client.BoundImage
"""
response = self._client.request(url=f"/images/{id}", method="GET")
return BoundImage(self, response["image"])
def get_list(
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]]
architecture=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
status=None, # type: Optional[List[str]]
include_deprecated=None, # type: Optional[bool]
):
# type: (...) -> 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 architecture: List[str] (optional)
Choices: x86 arm
: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 <hcloud.images.client.BoundImage>`], :class:`Meta <hcloud.core.domain.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 architecture is not None:
params["architecture"] = architecture
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]]
architecture=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 architecture: List[str] (optional)
Choices: x86 arm
: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 <hcloud.images.client.BoundImage>`]
"""
return super().get_all(
name=name,
label_selector=label_selector,
bound_to=bound_to,
type=type,
architecture=architecture,
sort=sort,
status=status,
include_deprecated=include_deprecated,
)
def get_by_name(self, name):
# type: (str) -> BoundImage
"""Get image by name
Deprecated: Use get_by_name_and_architecture instead.
:param name: str
Used to get image by name.
:return: :class:`BoundImage <hcloud.images.client.BoundImage>`
"""
return super().get_by_name(name)
def get_by_name_and_architecture(self, name, architecture):
# type: (str, str) -> BoundImage
"""Get image by name
:param name: str
Used to identify the image.
:param architecture: str
Used to identify the image.
:return: :class:`BoundImage <hcloud.images.client.BoundImage>`
"""
response = self.get_list(name=name, architecture=[architecture])
entities = getattr(response, self.results_list_attribute_name)
entity = entities[0] if entities else None
return entity
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 <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.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 <hcloud.images.client.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=f"/images/{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 <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>`
:return: bool
"""
self._client.request(url=f"/images/{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]) -> BoundAction
"""Changes the protection configuration of the image. Can only be used on snapshots.
:param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>`
:param delete: bool
If true, prevents the snapshot from being deleted
:return: :class:`BoundAction <hcloud.actions.client.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"])

View file

@ -0,0 +1,124 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 its 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 <hcloud.servers.client.BoundServer>`, None
Information about the server the image was created from
:param bound_to: :class:`BoundServer <hcloud.servers.client.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 architecture: str
CPU Architecture that the image is compatible with. Choices: `x86`, `arm`
: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",
"architecture",
"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,
architecture=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.architecture = architecture
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 <hcloud.images.client.BoundImage>`
The Image which was created
:param action: :class:`BoundAction <hcloud.actions.client.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

View file

View file

@ -0,0 +1,118 @@
from warnings import warn
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from .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 <hcloud.isos.client.BoundIso>`
"""
response = self._client.request(url=f"/isos/{id}", method="GET")
return BoundIso(self, response["iso"])
def get_list(
self,
name=None, # type: Optional[str]
architecture=None, # type: Optional[List[str]]
include_wildcard_architecture=None, # type: Optional[bool]
include_architecture_wildcard=None, # type: Optional[bool]
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 architecture: List[str] (optional)
Can be used to filter ISOs by their architecture. Choices: x86 arm
:param include_wildcard_architecture: bool (optional)
Deprecated, please use `include_architecture_wildcard` instead.
:param include_architecture_wildcard: bool (optional)
Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by
architecture and also want custom ISOs.
: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 <hcloud.isos.client.BoundIso>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
if include_wildcard_architecture is not None:
warn(
"The `include_wildcard_architecture` argument is deprecated, please use the `include_architecture_wildcard` argument instead.",
DeprecationWarning,
)
include_architecture_wildcard = include_wildcard_architecture
params = {}
if name is not None:
params["name"] = name
if architecture is not None:
params["architecture"] = architecture
if include_architecture_wildcard is not None:
params["include_architecture_wildcard"] = include_architecture_wildcard
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]
architecture=None, # type: Optional[List[str]]
include_wildcard_architecture=None, # type: Optional[bool]
include_architecture_wildcard=None, # type: Optional[bool]
):
# type: (...) -> List[BoundIso]
"""Get all ISOs
:param name: str (optional)
Can be used to filter ISOs by their name.
:param architecture: List[str] (optional)
Can be used to filter ISOs by their architecture. Choices: x86 arm
:param include_wildcard_architecture: bool (optional)
Deprecated, please use `include_architecture_wildcard` instead.
:param include_architecture_wildcard: bool (optional)
Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by
architecture and also want custom ISOs.
:return: List[:class:`BoundIso <hcloud.isos.client.BoundIso>`]
"""
if include_wildcard_architecture is not None:
warn(
"The `include_wildcard_architecture` argument is deprecated, please use the `include_architecture_wildcard` argument instead.",
DeprecationWarning,
)
include_architecture_wildcard = include_wildcard_architecture
return super().get_all(
name=name,
architecture=architecture,
include_architecture_wildcard=include_architecture_wildcard,
)
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 <hcloud.isos.client.BoundIso>`
"""
return super().get_by_name(name)

View file

@ -0,0 +1,42 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 architecture: str, None
CPU Architecture that the ISO is compatible with. None means that the compatibility is unknown. Choices: `x86`, `arm`
: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", "architecture", "description", "deprecated")
def __init__(
self,
id=None,
name=None,
type=None,
architecture=None,
description=None,
deprecated=None,
):
self.id = id
self.name = name
self.type = type
self.architecture = architecture
self.description = description
self.deprecated = isoparse(deprecated) if deprecated else None

View file

@ -0,0 +1,74 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from .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 <hcloud.load_balancer_type.client.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 <hcloud.load_balancer_types.client.BoundLoadBalancerType>`], :class:`Meta <hcloud.core.domain.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 <hcloud.load_balancer_types.client.BoundLoadBalancerType>`]
"""
return super().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 <hcloud.load_balancer_types.client.BoundLoadBalancerType>`
"""
return super().get_by_name(name)

View file

@ -0,0 +1,55 @@
from ..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

View file

@ -0,0 +1,900 @@
from ..actions.client import BoundAction
from ..certificates.client import BoundCertificate
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from ..load_balancer_types.client import BoundLoadBalancerType
from ..locations.client import BoundLocation
from ..networks.client import BoundNetwork
from ..servers.client import BoundServer
from .domain import (
CreateLoadBalancerResponse,
IPv4Address,
IPv6Network,
LoadBalancer,
LoadBalancerAlgorithm,
LoadBalancerHealtCheckHttp,
LoadBalancerHealthCheck,
LoadBalancerService,
LoadBalancerServiceHttp,
LoadBalancerTarget,
LoadBalancerTargetIP,
LoadBalancerTargetLabelSelector,
PrivateNet,
PublicNetwork,
)
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().__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 <hcloud.load_balancers.client.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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 <hcloud.actions.client.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 <hcloud.load_balancers.domain.LoadBalancerService>`
The LoadBalancerService you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.domain.LoadBalancerService>`
The LoadBalancerService you want to update
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.domain.LoadBalancerService>`
The LoadBalancerService you want to delete from the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.domain.LoadBalancerTarget>`
The LoadBalancerTarget you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.domain.LoadBalancerTarget>`
The LoadBalancerTarget you want to remove from the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.domain.LoadBalancerAlgorithm>`
The LoadBalancerAlgorithm you want to use
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:param ip: str
IP to request to be assigned to this Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.load_balancer_types.client.BoundLoadBalancerType>` or :class:`LoadBalancerType <hcloud.load_balancer_types.domain.LoadBalancerType>`
Load Balancer type the Load Balancer should migrate to
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.client.BoundLoadBalancer>`
"""
response = self._client.request(
url=f"/load_balancers/{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 <hcloud.load_balancers.client.BoundLoadBalancer>`], :class:`Meta <hcloud.core.domain.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 <hcloud.load_balancers.client.BoundLoadBalancer>`]
"""
return super().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 <hcloud.load_balancers.client.BoundLoadBalancer>`
"""
return super().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 <hcloud.load_balancers.domain.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 LoadBalancers name and a LoadBalancers labels.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param name: str (optional)
New name to set
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.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 <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.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 <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.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 <hcloud.actions.client.BoundAction>`]
"""
return super().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 <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>`
The LoadBalancerService you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>`
The LoadBalancerService with updated values within for the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>`
The LoadBalancerService you want to delete from the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>`
The LoadBalancerTarget you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>`
The LoadBalancerTarget you want to remove from the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param algorithm: :class:`LoadBalancerAlgorithm <hcloud.load_balancers.domain.LoadBalancerAlgorithm>`
The LoadBalancerSubnet you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.actions.client.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:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param delete: boolean
If True, prevents the Load Balancer from being deleted
:return: :class:`BoundAction <hcloud.actions.client.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:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:param ip: str
IP to request to be assigned to this Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.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:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:return: :class:`BoundAction <hcloud.actions.client.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:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:return: :class:`BoundAction <hcloud.actions.client.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:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param load_balancer_type: :class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>` or :class:`LoadBalancerType <hcloud.load_balancer_types.domain.LoadBalancerType>`
Load Balancer type the Load Balancer should migrate to
:return: :class:`BoundAction <hcloud.actions.client.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"])

View file

@ -0,0 +1,370 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 <hcloud.load_balancers.domain.PublicNetwork>`
Public network information.
:param private_net: List[:class:`PrivateNet <hcloud.load_balancers.domain.PrivateNet`]
Private networks information.
:param algorithm: LoadBalancerAlgorithm
The algorithm the Load Balancer is currently using
:param services: List[LoadBalancerService]
The services the LoadBalancer is currently serving
:param targets: LoadBalancerTarget
The targets the LoadBalancer is currently serving
:param load_balancer_type: LoadBalancerType
The type of the Load Balancer
: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
"""
__slots__ = (
"id",
"name",
"public_net",
"private_net",
"location",
"algorithm",
"services",
"load_balancer_type",
"protection",
"labels",
"targets",
"created",
"outgoing_traffic",
"ingoing_traffic",
"included_traffic",
)
def __init__(
self,
id,
name=None,
public_net=None,
private_net=None,
location=None,
algorithm=None,
services=None,
load_balancer_type=None,
protection=None,
labels=None,
targets=None,
created=None,
outgoing_traffic=None,
ingoing_traffic=None,
included_traffic=None,
):
self.id = id
self.name = name
self.created = isoparse(created) if created else None
self.public_net = public_net
self.private_net = private_net
self.location = location
self.algorithm = algorithm
self.services = services
self.load_balancer_type = load_balancer_type
self.targets = targets
self.protection = protection
self.labels = labels
self.outgoing_traffic = outgoing_traffic
self.ingoing_traffic = ingoing_traffic
self.included_traffic = included_traffic
class LoadBalancerService(BaseDomain):
"""LoadBalancerService Domain
:param protocol: str
Protocol of the service Choices: tcp, http, https
:param listen_port: int
Required when protocol is tcp, must be unique per Load Balancer.
:param destination_port: int
Required when protocol is tcp
:param proxyprotocol: bool
Enable proxyprotocol
:param health_check: LoadBalancerHealthCheck
Configuration for health checks
:param http: LoadBalancerServiceHttp
Configuration for http/https protocols, required when protocol is http/https
"""
def __init__(
self,
protocol=None,
listen_port=None,
destination_port=None,
proxyprotocol=None,
health_check=None,
http=None,
):
self.protocol = protocol
self.listen_port = listen_port
self.destination_port = destination_port
self.proxyprotocol = proxyprotocol
self.health_check = health_check
self.http = http
class LoadBalancerServiceHttp(BaseDomain):
"""LoadBalancerServiceHttp Domain
:param cookie_name: str
Name of the cookie used for Session Stickness
:param cookie_lifetime: str
Lifetime of the cookie used for Session Stickness
:param certificates: list
IDs of the Certificates to use for TLS/SSL termination by the Load Balancer; empty for TLS/SSL passthrough or if protocol is "http"
:param redirect_http: bool
Redirect traffic from http port 80 to port 443
:param sticky_sessions: bool
Use sticky sessions. Only available if protocol is "http" or "https".
"""
def __init__(
self,
cookie_name=None,
cookie_lifetime=None,
certificates=None,
redirect_http=None,
sticky_sessions=None,
):
self.cookie_name = cookie_name
self.cookie_lifetime = cookie_lifetime
self.certificates = certificates
self.redirect_http = redirect_http
self.sticky_sessions = sticky_sessions
class LoadBalancerHealthCheck(BaseDomain):
"""LoadBalancerHealthCheck Domain
:param protocol: str
Protocol of the service Choices: tcp, http, https
:param port: int
Port the healthcheck will be performed on
:param interval: int
Interval we trigger health check in
:param timeout: int
Timeout in sec after a try is assumed as timeout
:param retries: int
Retries we perform until we assume a target as unhealthy
:param http: LoadBalancerHealtCheckHttp
HTTP Config
"""
def __init__(
self,
protocol=None,
port=None,
interval=None,
timeout=None,
retries=None,
http=None,
):
self.protocol = protocol
self.port = port
self.interval = interval
self.timeout = timeout
self.retries = retries
self.http = http
class LoadBalancerHealtCheckHttp(BaseDomain):
"""LoadBalancerHealtCheckHttp Domain
:param domain: str
Domain name to send in HTTP request. Can be null: In that case we will not send a domain name
:param path: str
HTTP Path send in Request
:param response: str
Optional HTTP response to receive in order to pass the health check
:param status_codes: list
List of HTTP status codes to receive in order to pass the health check
:param tls: bool
Type of health check
"""
def __init__(
self, domain=None, path=None, response=None, status_codes=None, tls=None
):
self.domain = domain
self.path = path
self.response = response
self.status_codes = status_codes
self.tls = tls
class LoadBalancerTarget(BaseDomain):
"""LoadBalancerTarget Domain
:param type: str
Type of the resource, can be server or label_selector
:param server: Server
Target server
:param label_selector: LoadBalancerTargetLabelSelector
Target label selector
:param ip: LoadBalancerTargetIP
Target IP
:param use_private_ip: bool
use the private IP instead of primary public IP
"""
def __init__(
self, type=None, server=None, label_selector=None, ip=None, use_private_ip=None
):
self.type = type
self.server = server
self.label_selector = label_selector
self.ip = ip
self.use_private_ip = use_private_ip
class LoadBalancerTargetLabelSelector(BaseDomain):
"""LoadBalancerTargetLabelSelector Domain
:param selector: str Target label selector
"""
def __init__(self, selector=None):
self.selector = selector
class LoadBalancerTargetIP(BaseDomain):
"""LoadBalancerTargetIP Domain
:param ip: str Target IP
"""
def __init__(self, ip=None):
self.ip = ip
class LoadBalancerAlgorithm(BaseDomain):
"""LoadBalancerAlgorithm Domain
:param type: str
Algorithm of the Load Balancer. Choices: round_robin, least_connections
"""
def __init__(self, type=None):
self.type = type
class PublicNetwork(BaseDomain):
"""Public Network Domain
:param ipv4: :class:`IPv4Address <hcloud.load_balancers.domain.IPv4Address>`
:param ipv6: :class:`IPv6Network <hcloud.load_balancers.domain.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 <hcloud.networks.client.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 <hcloud.load_balancers.client.BoundLoadBalancer>`
The created Load Balancer
:param action: :class:`BoundAction <hcloud.actions.client.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

View file

@ -0,0 +1,67 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from .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 <hcloud.locations.client.BoundLocation>`
"""
response = self._client.request(url=f"/locations/{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 <hcloud.locations.client.BoundLocation>`], :class:`Meta <hcloud.core.domain.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 <hcloud.locations.client.BoundLocation>`]
"""
return super().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 <hcloud.locations.client.BoundLocation>`
"""
return super().get_by_name(name)

View file

@ -0,0 +1,54 @@
from ..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

View file

@ -0,0 +1,499 @@
from ..actions.client import BoundAction
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from .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 ..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().__init__(client, data, complete)
def update(
self,
name=None, # type: Optional[str]
expose_routes_to_vswitch=None, # type: Optional[bool]
labels=None, # type: Optional[Dict[str, str]]
): # type: (...) -> BoundNetwork
"""Updates a network. You can update a networks name and a networkss labels.
:param name: str (optional)
New name to set
:param expose_routes_to_vswitch: Optional[bool]
Indicates if the routes from this network should be exposed to the vSwitch connection.
The exposing only takes effect if a vSwitch connection is active.
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`
"""
return self._client.update(
self,
name=name,
expose_routes_to_vswitch=expose_routes_to_vswitch,
labels=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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 <hcloud.actions.client.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 <hcloud.networks.domain.NetworkSubnet>`
The NetworkSubnet you want to add to the Network
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.networks.domain.NetworkSubnet>`
The NetworkSubnet you want to remove from the Network
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.networks.domain.NetworkRoute>`
The NetworkRoute you want to add to the Network
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.networks.domain.NetworkRoute>`
The NetworkRoute you want to remove from the Network
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.networks.client.BoundNetwork>
"""
response = self._client.request(url=f"/networks/{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 <hcloud.networks.client.BoundNetwork>`], :class:`Meta <hcloud.core.domain.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 <hcloud.networks.client.BoundNetwork>`]
"""
return super().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 <hcloud.networks.client.BoundNetwork>`
"""
return super().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]]
expose_routes_to_vswitch=None, # type: Optional[bool]
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 <hcloud.networks.domain.NetworkSubnet>`]
Array of subnets allocated
:param routes: List[:class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`]
Array of routes set in this network
:param expose_routes_to_vswitch: Optional[bool]
Indicates if the routes from this network should be exposed to the vSwitch connection.
The exposing only takes effect if a vSwitch connection is active.
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`
"""
data = {"name": name, "ip_range": ip_range}
if subnets is not None:
data_subnets = []
for subnet in subnets:
data_subnet = {
"type": subnet.type,
"ip_range": subnet.ip_range,
"network_zone": subnet.network_zone,
}
if subnet.vswitch_id is not None:
data_subnet["vswitch_id"] = subnet.vswitch_id
data_subnets.append(data_subnet)
data["subnets"] = data_subnets
if routes is not None:
data["routes"] = [
{"destination": route.destination, "gateway": route.gateway}
for route in routes
]
if expose_routes_to_vswitch is not None:
data["expose_routes_to_vswitch"] = expose_routes_to_vswitch
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, expose_routes_to_vswitch=None, labels=None):
# type:(Network, Optional[str], Optional[bool], Optional[Dict[str, str]]) -> BoundNetwork
"""Updates a network. You can update a networks name and a networks labels.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:param name: str (optional)
New name to set
:param expose_routes_to_vswitch: Optional[bool]
Indicates if the routes from this network should be exposed to the vSwitch connection.
The exposing only takes effect if a vSwitch connection is active.
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`
"""
data = {}
if name is not None:
data.update({"name": name})
if expose_routes_to_vswitch is not None:
data["expose_routes_to_vswitch"] = expose_routes_to_vswitch
if labels is not None:
data.update({"labels": labels})
response = self._client.request(
url=f"/networks/{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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:return: boolean
"""
self._client.request(url=f"/networks/{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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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=f"/networks/{network.id}/actions",
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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.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 <hcloud.actions.client.BoundAction>`]
"""
return super().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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>`
The NetworkSubnet you want to add to the Network
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>`
The NetworkSubnet you want to remove from the Network
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`
The NetworkRoute you want to add to the Network
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`
The NetworkRoute you want to remove from the Network
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:param ip_range: str
The new prefix for the whole network.
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:param delete: boolean
If True, prevents the network from being deleted
:return: :class:`BoundAction <hcloud.actions.client.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"])

View file

@ -0,0 +1,136 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 <hcloud.networks.domain.NetworkSubnet>`]
Subnets allocated in this network
:param routes: List[:class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`]
Routes set in this network
:param expose_routes_to_vswitch: bool
Indicates if the routes from this network should be exposed to the vSwitch connection.
:param servers: List[:class:`BoundServer <hcloud.servers.client.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",
"expose_routes_to_vswitch",
"servers",
"protection",
"labels",
"created",
)
def __init__(
self,
id,
name=None,
created=None,
ip_range=None,
subnets=None,
routes=None,
expose_routes_to_vswitch=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.expose_routes_to_vswitch = expose_routes_to_vswitch
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 <hcloud.networks.client.BoundNetwork>`
The network which was created
:param action: :class:`BoundAction <hcloud.actions.client.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

View file

@ -0,0 +1,194 @@
from ..actions.client import BoundAction
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from .domain import CreatePlacementGroupResponse, PlacementGroup
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 <hcloud.placement_groups.client.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 <hcloud.placement_groups.client.BoundPlacementGroup>`
"""
response = self._client.request(
url=f"/placement_groups/{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 <hcloud.placement_groups.client.BoundPlacementGroup>`], :class:`Meta <hcloud.core.domain.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 <hcloud.placement_groups.client.BoundPlacementGroup>`]
"""
return super().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 <hcloud.placement_groups.client.BoundPlacementGroup>`
"""
return super().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 <hcloud.placement_groups.domain.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 <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`PlacementGroup <hcloud.placement_groups.domain.PlacementGroup>`
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:param name: str (optional)
New name to set
:return: :class:`BoundPlacementGroup <hcloud.placement_groups.client.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 <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`PlacementGroup <hcloud.placement_groups.domain.PlacementGroup>`
:return: boolean
"""
self._client.request(
url="/placement_groups/{placement_group_id}".format(
placement_group_id=placement_group.id
),
method="DELETE",
)
return True

View file

@ -0,0 +1,61 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 <hcloud.placement_groups.client.BoundPlacementGroup>`
The Placement Group which was created
:param action: :class:`BoundAction <hcloud.actions.client.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

View file

@ -0,0 +1,329 @@
from ..actions.client import BoundAction
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from .domain import CreatePrimaryIPResponse, PrimaryIP
class BoundPrimaryIP(BoundModelBase):
model = PrimaryIP
def __init__(self, client, data, complete=True):
from ..datacenters.client import BoundDatacenter
datacenter = data.get("datacenter", {})
if datacenter:
data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter)
super().__init__(client, data, complete)
def update(self, auto_delete=None, labels=None, name=None):
# type: (Optional[bool], Optional[Dict[str, str]], Optional[str]) -> BoundPrimaryIP
"""Updates the description or labels of a Primary IP.
:param auto_delete: bool (optional)
Auto delete IP when assignee gets deleted
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:param name: str (optional)
New Name to set
:return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`
"""
return self._client.update(
self, auto_delete=auto_delete, labels=labels, name=name
)
def delete(self):
# type: () -> bool
"""Deletes a Primary 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 Primary IP.
:param delete: boolean
If true, prevents the Primary IP from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
return self._client.change_protection(self, delete)
def assign(self, assignee_id, assignee_type):
# type: (int,str) -> BoundAction
"""Assigns a Primary IP to a assignee.
:param assignee_id: int`
Id of an assignee the Primary IP shall be assigned to
:param assignee_type: string`
Assignee type (e.g server) the Primary IP shall be assigned to
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
return self._client.assign(self, assignee_id, assignee_type)
def unassign(self):
# type: () -> BoundAction
"""Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time.
:return: :class:`BoundAction <hcloud.actions.client.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 Primary 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 <hcloud.actions.client.BoundAction>`
"""
return self._client.change_dns_ptr(self, ip, dns_ptr)
class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "primary_ips"
def get_by_id(self, id):
# type: (int) -> BoundPrimaryIP
"""Returns a specific Primary IP object.
:param id: int
:return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`
"""
response = self._client.request(url=f"/primary_ips/{id}", method="GET")
return BoundPrimaryIP(self, response["primary_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]
ip=None, # type: Optional[ip]
):
# type: (...) -> PageResults[List[BoundPrimaryIP]]
"""Get a list of primary ips from this account
:param label_selector: str (optional)
Can be used to filter Primary IPs by labels. The response will only contain Primary IPs matching the label selectorable 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 ip: str (optional)
Can be used to filter resources by their ip. The response will only contain the resources matching the specified ip.
:return: (List[:class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`], :class:`Meta <hcloud.core.domain.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 ip is not None:
params["ip"] = ip
response = self._client.request(url="/primary_ips", method="GET", params=params)
primary_ips = [
BoundPrimaryIP(self, primary_ip_data)
for primary_ip_data in response["primary_ips"]
]
return self._add_meta_to_result(primary_ips, response)
def get_all(self, label_selector=None, name=None):
# type: (Optional[str], Optional[str]) -> List[BoundPrimaryIP]
"""Get all primary ips from this account
:param label_selector: str (optional)
Can be used to filter Primary IPs by labels. The response will only contain Primary IPs matching the label selector.able values.
:param name: str (optional)
Can be used to filter networks by their name.
:return: List[:class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`]
"""
return super().get_all(label_selector=label_selector, name=name)
def get_by_name(self, name):
# type: (str) -> BoundPrimaryIP
"""Get Primary IP by name
:param name: str
Used to get Primary IP by name.
:return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`
"""
return super().get_by_name(name)
def create(
self,
type, # type: str
datacenter, # type: Datacenter
name, # type: str
assignee_type="server", # type: Optional[str]
assignee_id=None, # type: Optional[int]
auto_delete=False, # type: Optional[bool]
labels=None, # type: Optional[dict]
):
# type: (...) -> CreatePrimaryIPResponse
"""Creates a new Primary IP assigned to a server.
:param type: str
Primary IP type Choices: ipv4, ipv6
:param assignee_type: str
:param assignee_id: int (optional)
:param datacenter: Datacenter
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:param name: str
:param auto_delete: bool (optional)
:return: :class:`CreatePrimaryIPResponse <hcloud.primary_ips.domain.CreatePrimaryIPResponse>`
"""
data = {
"type": type,
"assignee_type": assignee_type,
"auto_delete": auto_delete,
"datacenter": datacenter.id_or_name,
"name": name,
}
if assignee_id:
data["assignee_id"] = assignee_id
if labels is not None:
data["labels"] = labels
response = self._client.request(url="/primary_ips", json=data, method="POST")
action = None
if response.get("action") is not None:
action = BoundAction(self._client.actions, response["action"])
result = CreatePrimaryIPResponse(
primary_ip=BoundPrimaryIP(self, response["primary_ip"]), action=action
)
return result
def update(self, primary_ip, auto_delete=None, labels=None, name=None):
# type: (PrimaryIP, Optional[bool], Optional[Dict[str, str]], Optional[str]) -> BoundPrimaryIP
"""Updates the name, auto_delete or labels of a Primary IP.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
:param auto_delete: bool (optional)
Delete this Primary IP when the resource it is assigned to is deleted
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:param name: str (optional)
New name to set
:return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`
"""
data = {}
if auto_delete is not None:
data["auto_delete"] = auto_delete
if labels is not None:
data["labels"] = labels
if name is not None:
data["name"] = name
response = self._client.request(
url=f"/primary_ips/{primary_ip.id}",
method="PUT",
json=data,
)
return BoundPrimaryIP(self, response["primary_ip"])
def delete(self, primary_ip):
# type: (PrimaryIP) -> bool
"""Deletes a Primary IP. If it is currently assigned to an assignee it will automatically get unassigned.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
:return: boolean
"""
self._client.request(
url=f"/primary_ips/{primary_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, primary_ip, delete=None):
# type: (PrimaryIP, Optional[bool]) -> BoundAction
"""Changes the protection configuration of the Primary IP.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
:param delete: boolean
If true, prevents the Primary IP from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {}
if delete is not None:
data.update({"delete": delete})
response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/change_protection".format(
primary_ip_id=primary_ip.id
),
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def assign(self, primary_ip, assignee_id, assignee_type="server"):
# type: (PrimaryIP, int, str) -> BoundAction
"""Assigns a Primary IP to a assignee_id.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
:param assignee_id: int
Assignee the Primary IP shall be assigned to
:param assignee_type: str
Assignee the Primary IP shall be assigned to
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/assign".format(
primary_ip_id=primary_ip.id
),
method="POST",
json={"assignee_id": assignee_id, "assignee_type": assignee_type},
)
return BoundAction(self._client.actions, response["action"])
def unassign(self, primary_ip):
# type: (PrimaryIP) -> BoundAction
"""Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/unassign".format(
primary_ip_id=primary_ip.id
),
method="POST",
)
return BoundAction(self._client.actions, response["action"])
def change_dns_ptr(self, primary_ip, ip, dns_ptr):
# type: (PrimaryIP, str, str) -> BoundAction
"""Changes the dns ptr that will appear when getting the dns ptr belonging to this Primary IP.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
: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 <hcloud.actions.client.BoundAction>`
"""
response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/change_dns_ptr".format(
primary_ip_id=primary_ip.id
),
method="POST",
json={"ip": ip, "dns_ptr": dns_ptr},
)
return BoundAction(self._client.actions, response["action"])

View file

@ -0,0 +1,104 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..core.domain import BaseDomain
class PrimaryIP(BaseDomain):
"""Primary IP Domain
:param id: int
ID of the Primary IP
:param ip: str
IP address of the Primary IP
:param type: str
Type of Primary IP. Choices: `ipv4`, `ipv6`
:param dns_ptr: List[Dict]
Array of reverse DNS entries
:param datacenter: :class:`Datacenter <hcloud.datacenters.client.BoundDatacenter>`
Datacenter the Primary IP was created in.
:param blocked: boolean
Whether the IP is blocked
:param protection: dict
Protection configuration for the Primary IP
:param labels: dict
User-defined labels (key-value pairs)
:param created: datetime
Point in time when the Primary IP was created
:param name: str
Name of the Primary IP
:param assignee_id: int
Assignee ID the Primary IP is assigned to
:param assignee_type: str
Assignee Type of entity the Primary IP is assigned to
:param auto_delete: bool
Delete the Primary IP when the Assignee it is assigned to is deleted.
"""
__slots__ = (
"id",
"ip",
"type",
"dns_ptr",
"datacenter",
"blocked",
"protection",
"labels",
"created",
"name",
"assignee_id",
"assignee_type",
"auto_delete",
)
def __init__(
self,
id=None,
type=None,
ip=None,
dns_ptr=None,
datacenter=None,
blocked=None,
protection=None,
labels=None,
created=None,
name=None,
assignee_id=None,
assignee_type=None,
auto_delete=None,
):
self.id = id
self.type = type
self.ip = ip
self.dns_ptr = dns_ptr
self.datacenter = datacenter
self.blocked = blocked
self.protection = protection
self.labels = labels
self.created = isoparse(created) if created else None
self.name = name
self.assignee_id = assignee_id
self.assignee_type = assignee_type
self.auto_delete = auto_delete
class CreatePrimaryIPResponse(BaseDomain):
"""Create Primary IP Response Domain
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`
The Primary IP which was created
:param action: :class:`BoundAction <hcloud.actions.client.BoundAction>`
The Action which shows the progress of the Primary IP Creation
"""
__slots__ = ("primary_ip", "action")
def __init__(
self,
primary_ip, # type: BoundPrimaryIP
action, # type: BoundAction
):
self.primary_ip = primary_ip
self.action = action

View file

@ -0,0 +1,69 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from .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) -> BoundServerType
"""Returns a specific Server Type.
:param id: int
:return: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>`
"""
response = self._client.request(url=f"/server_types/{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 <hcloud.server_types.client.BoundServerType>`], :class:`Meta <hcloud.core.domain.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 <hcloud.server_types.client.BoundServerType>`]
"""
return super().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 <hcloud.server_types.client.BoundServerType>`
"""
return super().get_by_name(name)

View file

@ -0,0 +1,83 @@
from ..core.domain import BaseDomain, DomainIdentityMixin
from ..deprecation.domain import DeprecationInfo
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 architecture: string
Architecture of cpu. Choices: `x86`, `arm`
:param deprecated: bool
True if server type is deprecated. This field is deprecated. Use `deprecation` instead.
:param deprecation: :class:`DeprecationInfo <hcloud.deprecation.domain.DeprecationInfo>`, None
Describes if, when & how the resources was deprecated. If this field is set to None the resource is not
deprecated. If it has a value, it is considered deprecated.
:param included_traffic: int
Free traffic per month in bytes
"""
__slots__ = (
"id",
"name",
"description",
"cores",
"memory",
"disk",
"prices",
"storage_type",
"cpu_type",
"architecture",
"deprecated",
"deprecation",
"included_traffic",
)
def __init__(
self,
id=None,
name=None,
description=None,
cores=None,
memory=None,
disk=None,
prices=None,
storage_type=None,
cpu_type=None,
architecture=None,
deprecated=None,
deprecation=None,
included_traffic=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.architecture = architecture
self.deprecated = deprecated
self.deprecation = (
DeprecationInfo.from_dict(deprecation) if deprecation is not None else None
)
self.included_traffic = included_traffic

View file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,395 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 <hcloud.servers.domain.PublicNetwork>`
Public network information.
:param server_type: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>`
:param datacenter: :class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>`
:param image: :class:`BoundImage <hcloud.images.client.BoundImage>`, None
:param iso: :class:`BoundIso <hcloud.isos.client.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 <hcloud.volumes.client.BoundVolume>`]
Volumes assigned to this server.
:param private_net: List[:class:`PrivateNet <hcloud.servers.domain.PrivateNet>`]
Private networks information.
"""
STATUS_RUNNING = "running"
"""Server Status running"""
STATUS_INIT = "initializing"
"""Server Status initializing"""
STATUS_STARTING = "starting"
"""Server Status starting"""
STATUS_STOPPING = "stopping"
"""Server Status stopping"""
STATUS_OFF = "off"
"""Server Status off"""
STATUS_DELETING = "deleting"
"""Server Status deleting"""
STATUS_MIGRATING = "migrating"
"""Server Status migrating"""
STATUS_REBUILDING = "rebuilding"
"""Server Status rebuilding"""
STATUS_UNKNOWN = "unknown"
"""Server Status unknown"""
__slots__ = (
"id",
"name",
"status",
"public_net",
"server_type",
"datacenter",
"image",
"iso",
"rescue_enabled",
"locked",
"backup_window",
"outgoing_traffic",
"ingoing_traffic",
"included_traffic",
"protection",
"labels",
"volumes",
"private_net",
"created",
"primary_disk_size",
"placement_group",
)
def __init__(
self,
id,
name=None,
status=None,
created=None,
public_net=None,
server_type=None,
datacenter=None,
image=None,
iso=None,
rescue_enabled=None,
locked=None,
backup_window=None,
outgoing_traffic=None,
ingoing_traffic=None,
included_traffic=None,
protection=None,
labels=None,
volumes=None,
private_net=None,
primary_disk_size=None,
placement_group=None,
):
self.id = id
self.name = name
self.status = status
self.created = isoparse(created) if created else None
self.public_net = public_net
self.server_type = server_type
self.datacenter = datacenter
self.image = image
self.iso = iso
self.rescue_enabled = rescue_enabled
self.locked = locked
self.backup_window = backup_window
self.outgoing_traffic = outgoing_traffic
self.ingoing_traffic = ingoing_traffic
self.included_traffic = included_traffic
self.protection = protection
self.labels = labels
self.volumes = volumes
self.private_net = private_net
self.primary_disk_size = primary_disk_size
self.placement_group = placement_group
class CreateServerResponse(BaseDomain):
"""Create Server Response Domain
:param server: :class:`BoundServer <hcloud.servers.client.BoundServer>`
The created server
:param action: :class:`BoundAction <hcloud.actions.client.BoundAction>`
Shows the progress of the server creation
:param next_actions: List[:class:`BoundAction <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.servers.domain.IPv4Address>`
:param ipv6: :class:`IPv6Network <hcloud.servers.domain.IPv6Network>`
:param floating_ips: List[:class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`]
:param primary_ipv4: :class:`BoundPrimaryIP <hcloud.primary_ips.domain.BoundPrimaryIP>`
:param primary_ipv6: :class:`BoundPrimaryIP <hcloud.primary_ips.domain.BoundPrimaryIP>`
:param firewalls: List[:class:`PublicNetworkFirewall <hcloud.servers.client.PublicNetworkFirewall>`]
"""
__slots__ = (
"ipv4",
"ipv6",
"floating_ips",
"firewalls",
"primary_ipv4",
"primary_ipv6",
)
def __init__(
self,
ipv4, # type: IPv4Address
ipv6, # type: IPv6Network
floating_ips, # type: List[BoundFloatingIP]
primary_ipv4, # type: BoundPrimaryIP
primary_ipv6, # type: BoundPrimaryIP
firewalls=None, # type: List[PublicNetworkFirewall]
):
self.ipv4 = ipv4
self.ipv6 = ipv6
self.floating_ips = floating_ips
self.firewalls = firewalls
self.primary_ipv4 = primary_ipv4
self.primary_ipv6 = primary_ipv6
class PublicNetworkFirewall(BaseDomain):
"""Public Network Domain
:param firewall: :class:`BoundFirewall <hcloud.firewalls.domain.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 <hcloud.networks.client.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
class ServerCreatePublicNetwork(BaseDomain):
"""Server Create Public Network Domain
:param ipv4: Optional[:class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`]
:param ipv6: Optional[:class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`]
:param enable_ipv4: bool
:param enable_ipv6: bool
"""
__slots__ = ("ipv4", "ipv6", "enable_ipv4", "enable_ipv6")
def __init__(
self,
ipv4=None, # type: hcloud.primary_ips.domain.PrimaryIP
ipv6=None, # type: hcloud.primary_ips.domain.PrimaryIP
enable_ipv4=True, # type: bool
enable_ipv6=True, # type: bool
):
self.ipv4 = ipv4
self.ipv6 = ipv6
self.enable_ipv4 = enable_ipv4
self.enable_ipv6 = enable_ipv6

View file

@ -0,0 +1,170 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from .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 <hcloud.ssh_keys.client.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 <hcloud.ssh_keys.client.BoundSSHKey>`
"""
response = self._client.request(url=f"/ssh_keys/{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 <hcloud.ssh_keys.client.BoundSSHKey>`], :class:`Meta <hcloud.core.domain.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 <hcloud.ssh_keys.client.BoundSSHKey>`]
"""
return super().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 <hcloud.ssh_keys.client.BoundSSHKey>`
"""
return super().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 <hcloud.ssh_keys.client.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 <hcloud.ssh_keys.client.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 <hcloud.ssh_keys.client.BoundSSHKey>` or :class:`SSHKey <hcloud.ssh_keys.domain.SSHKey>`
:param name: str (optional)
New Description to set
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`
"""
data = {}
if name is not None:
data["name"] = name
if labels is not None:
data["labels"] = labels
response = self._client.request(
url=f"/ssh_keys/{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=f"/ssh_keys/{ssh_key.id}", method="DELETE")
"""Deletes an SSH key. It cannot be used anymore.
:param ssh_key: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` or :class:`SSHKey <hcloud.ssh_keys.domain.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

View file

@ -0,0 +1,42 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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

View file

View file

@ -0,0 +1,395 @@
from ..actions.client import BoundAction
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from ..locations.client import BoundLocation
from .domain import CreateVolumeResponse, Volume
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 ..servers.client import BoundServer
server = data.get("server")
if server is not None:
data["server"] = BoundServer(
client._client.servers, {"id": server}, complete=False
)
super().__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 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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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 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 <hcloud.actions.client.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 <hcloud.actions.client.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], Optional[bool]) -> BoundAction
"""Attaches a volume to a server. Works only if the server is in the same location as the volume.
:param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>`
:param automount: boolean
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
return self._client.attach(self, server, automount)
def detach(self):
# type: () -> BoundAction
"""Detaches a volume from the server its attached to. You may attach it to a server again at a later time.
:return: :class:`BoundAction <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.actions.client.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 <hcloud.volumes.client.BoundVolume>`
"""
response = self._client.request(url=f"/volumes/{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 <hcloud.volumes.client.BoundVolume>`], :class:`Meta <hcloud.core.domain.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 <hcloud.volumes.client.BoundVolume>`]
"""
return super().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 <hcloud.volumes.client.BoundVolume>`
"""
return super().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 <hcloud.locations.client.BoundLocation>` or :class:`Location <hcloud.locations.domain.Location>`
:param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.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 <hcloud.volumes.domain.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[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta]
"""Returns all action objects for a volume.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.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 <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.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=f"/volumes/{volume.id}/actions",
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]], Optional[List[str]]) -> List[BoundAction]
"""Returns all action objects for a volume.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.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 <hcloud.actions.client.BoundAction>`]
"""
return super().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 <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
:param name: str (optional)
New volume name
:param labels: Dict[str, str] (optional)
User-defined labels (key-value pairs)
:return: :class:`BoundAction <hcloud.actions.client.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=f"/volumes/{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 <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
:return: boolean
"""
self._client.request(url=f"/volumes/{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 <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
:param size: int
New volume size in GB (must be greater than current size)
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = self._client.request(
url=f"/volumes/{volume.id}/actions/resize",
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 <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
:param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>`
:param automount: boolean
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {"server": server.id}
if automount is not None:
data["automount"] = automount
data = self._client.request(
url=f"/volumes/{volume.id}/actions/attach",
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 its attached to. You may attach it to a server again at a later time.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = self._client.request(
url=f"/volumes/{volume.id}/actions/detach",
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 <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
:param delete: boolean
If True, prevents the volume from being deleted
:return: :class:`BoundAction <hcloud.actions.client.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"])

View file

@ -0,0 +1,103 @@
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..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 <hcloud.servers.client.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 <hcloud.locations.client.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 <hcloud.volumes.client.BoundVolume>`
The created volume
:param action: :class:`BoundAction <hcloud.actions.client.BoundAction>`
The action that shows the progress of the Volume Creation
:param next_actions: List[:class:`BoundAction <hcloud.actions.client.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

View file

@ -171,13 +171,12 @@ import time
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud import (
try: APIException,
from hcloud import APIException )
from hcloud.firewalls.domain import FirewallRule from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.firewalls.domain import (
except ImportError: FirewallRule,
APIException = None )
FirewallRule = None
class AnsibleHcloudFirewall(Hcloud): class AnsibleHcloudFirewall(Hcloud):

View file

@ -282,17 +282,15 @@ hcloud_load_balancer_service:
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud import (
try: APIException,
from hcloud import APIException )
from hcloud.load_balancers.domain import ( from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.load_balancers.domain import (
LoadBalancerHealtCheckHttp, LoadBalancerHealtCheckHttp,
LoadBalancerHealthCheck, LoadBalancerHealthCheck,
LoadBalancerService, LoadBalancerService,
LoadBalancerServiceHttp, LoadBalancerServiceHttp,
) )
except ImportError:
APIException = None
class AnsibleHcloudLoadBalancerService(Hcloud): class AnsibleHcloudLoadBalancerService(Hcloud):

View file

@ -138,17 +138,11 @@ hcloud_load_balancer_target:
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.load_balancers.domain import (
try: LoadBalancerTarget,
from hcloud.load_balancers.domain import ( LoadBalancerTargetIP,
LoadBalancerTarget, LoadBalancerTargetLabelSelector,
LoadBalancerTargetIP, )
LoadBalancerTargetLabelSelector,
)
except ImportError:
LoadBalancerTarget = None
LoadBalancerTargetLabelSelector = None
LoadBalancerTargetIP = None
class AnsibleHcloudLoadBalancerTarget(Hcloud): class AnsibleHcloudLoadBalancerTarget(Hcloud):

View file

@ -90,11 +90,9 @@ hcloud_route:
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.networks.domain import (
try: NetworkRoute,
from hcloud.networks.domain import NetworkRoute )
except ImportError:
NetworkRoute = None
class AnsibleHcloudRoute(Hcloud): class AnsibleHcloudRoute(Hcloud):

View file

@ -333,18 +333,19 @@ from datetime import datetime, timedelta, timezone
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.firewalls.domain import (
try: FirewallResource,
from hcloud.firewalls.domain import FirewallResource )
from hcloud.servers.domain import Server, ServerCreatePublicNetwork from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.servers.domain import (
from hcloud.ssh_keys.domain import SSHKey Server,
from hcloud.volumes.domain import Volume ServerCreatePublicNetwork,
except ImportError: )
Volume = None from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.ssh_keys.domain import (
SSHKey = None SSHKey,
Server = None )
ServerCreatePublicNetwork = None from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.volumes.domain import (
FirewallResource = None Volume,
)
class AnsibleHcloudServer(Hcloud): class AnsibleHcloudServer(Hcloud):

View file

@ -115,11 +115,9 @@ hcloud_server_network:
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud import (
try: APIException,
from hcloud import APIException )
except ImportError:
APIException = None
class AnsibleHcloudServerNetwork(Hcloud): class AnsibleHcloudServerNetwork(Hcloud):

View file

@ -127,11 +127,9 @@ hcloud_subnetwork:
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native
from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.networks.domain import (
try: NetworkSubnet,
from hcloud.networks.domain import NetworkSubnet )
except ImportError:
NetworkSubnet = None
class AnsibleHcloudSubnetwork(Hcloud): class AnsibleHcloudSubnetwork(Hcloud):

103
scripts/vendor.py Executable file
View file

@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
Fetch and bundles the hcloud package inside the collection.
Fetch the desired version `HCLOUD_VERSION` from https://github.com/hetznercloud/hcloud-python
`HCLOUD_SOURCE_URL` using git, apply some code modifications to comply with ansible,
move the modified files at the vendor location `HCLOUD_VENDOR_PATH`.
"""
import logging
import re
from pathlib import Path
from shutil import move, rmtree
from subprocess import check_call
from tempfile import TemporaryDirectory
from textwrap import dedent
logger = logging.getLogger("vendor")
HCLOUD_SOURCE_URL = "https://github.com/hetznercloud/hcloud-python"
HCLOUD_VERSION = "v1.24.0"
HCLOUD_VENDOR_PATH = "plugins/module_utils/vendor/hcloud"
def apply_code_modifications(source_path: Path):
# The ansible galaxy-importer consider __version___.py to be an invalid filename in module_utils/
# Move the __version__.py file to _version.py
move(source_path / "__version__.py", source_path / "_version.py")
for file in source_path.rglob("*.py"):
content = file.read_text()
content_orig = content
# Move the __version__.py file to _version.py
content = re.sub(
r"from .__version__ import VERSION",
r"from ._version import VERSION",
content,
)
# Wrap requests imports
content = re.sub(
r"import requests",
dedent(
r"""
try:
import requests
except ImportError:
requests = None
"""
).strip(),
content,
)
# Wrap dateutil imports
content = re.sub(
r"from dateutil.parser import isoparse",
dedent(
r"""
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
"""
).strip(),
content,
)
# Remove requests.Response typings
content = re.sub(
r": requests\.Response",
r"",
content,
)
if content != content_orig:
logger.info("Applied code modifications on %s", file)
file.write_text(content)
def main() -> int:
with TemporaryDirectory() as tmp_dir:
tmp_dir_path = Path(tmp_dir)
logger.info("Created temporary directory %s", tmp_dir_path)
check_call(["git", "clone", "--depth=1", "--branch", HCLOUD_VERSION, HCLOUD_SOURCE_URL, tmp_dir_path])
logger.info("Cloned the source files in %s", tmp_dir_path)
apply_code_modifications(tmp_dir_path / "hcloud")
logger.info("Applied code modifications on the source files")
rmtree(HCLOUD_VENDOR_PATH)
move(tmp_dir_path / "hcloud", HCLOUD_VENDOR_PATH)
logger.info("Bundled the modified sources files in the collection")
return 0
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(levelname)-8s: %(message)s")
raise SystemExit(main())

View file

@ -1 +1,2 @@
hcloud >= 1.10.0 # minimum version python-dateutil>=2.7.5
requests>=2.20

View file

@ -1,2 +1,3 @@
netaddr netaddr
hcloud python-dateutil
requests

View file

@ -59,7 +59,8 @@ retry ansible-galaxy -vvv collection install community.general
retry ansible-galaxy -vvv collection install ansible.netcommon retry ansible-galaxy -vvv collection install ansible.netcommon
retry ansible-galaxy -vvv collection install community.internal_test_tools retry ansible-galaxy -vvv collection install community.internal_test_tools
retry pip install netaddr --disable-pip-version-check retry pip install netaddr --disable-pip-version-check
retry pip install hcloud retry pip install python-dateutil
retry pip install requests
retry pip install rstcheck retry pip install rstcheck
# END: HACK # END: HACK

View file

@ -43,5 +43,7 @@ pip install pylint==2.5.3
# shellcheck disable=SC2086 # shellcheck disable=SC2086
ansible-test sanity --color -v --junit ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \ ansible-test sanity --color -v --junit ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \
--base-branch "${base_branch}" \ --base-branch "${base_branch}" \
--exclude plugins/module_utils/vendor/ \
--exclude scripts/ \
--exclude tests/utils/ \ --exclude tests/utils/ \
"${options[@]}" --allow-disabled "${options[@]}" --allow-disabled

View file

@ -24,4 +24,6 @@ fi
# shellcheck disable=SC2086 # shellcheck disable=SC2086
ansible-test sanity --color -v --junit ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \ ansible-test sanity --color -v --junit ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \
--docker --base-branch "${base_branch}" \ --docker --base-branch "${base_branch}" \
--exclude plugins/module_utils/vendor/ \
--exclude scripts/ \
--allow-disabled --allow-disabled

View file

@ -77,7 +77,8 @@ fi
retry ansible-galaxy -vvv collection install community.general retry ansible-galaxy -vvv collection install community.general
retry ansible-galaxy -vvv collection install ansible.netcommon retry ansible-galaxy -vvv collection install ansible.netcommon
retry pip install hcloud retry pip install python-dateutil
retry pip install requests
retry pip install netaddr --disable-pip-version-check retry pip install netaddr --disable-pip-version-check
retry ansible-galaxy -vvv collection install community.internal_test_tools retry ansible-galaxy -vvv collection install community.internal_test_tools
# END: HACK # END: HACK