Skip to content

Commit

Permalink
feat: vendor hcloud python dependency (#244)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jooola committed Jul 13, 2023
1 parent bf9700a commit 8c7cd1e
Show file tree
Hide file tree
Showing 84 changed files with 8,432 additions and 78 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -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
# Edit at https://www.gitignore.io/?templates=git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv
Expand Down Expand Up @@ -55,7 +57,6 @@ flycheck_*.el
# network security
/network-security.data


### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
Expand Down Expand Up @@ -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

cloud-config-hcloud.ini
tests/integration/inventory
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
exclude: ^plugins/module_utils/vendor/hcloud/.*$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
Expand Down Expand Up @@ -52,3 +53,11 @@ repos:
entry: antsibull-changelog lint
pass_filenames: false
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 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions changelogs/fragments/vendor-hcloud-python-dependency.yml
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion plugins/doc_fragments/hcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ class ModuleDocFragment:
default: https://api.hetzner.cloud/v1
type: str
requirements:
- hcloud-python >= 1.20.0
- python-dateutil >= 2.7.5
- requests >=2.20
seealso:
- name: Documentation for Hetzner Cloud API
description: Complete reference for the Hetzner Cloud API.
Expand Down
22 changes: 11 additions & 11 deletions plugins/inventory/hcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,11 @@
from ansible.module_utils.common.text.converters import to_native
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.release import __version__

try:
from hcloud import APIException, hcloud

HAS_HCLOUD = True
except ImportError:
HAS_HCLOUD = False
from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import (
HAS_DATEUTIL,
HAS_REQUESTS,
)
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor import hcloud


class InventoryModule(BaseInventoryPlugin, Constructable):
Expand Down Expand Up @@ -159,7 +157,7 @@ def _test_hcloud_token(self):
# We test the API Token against the location API, because this is the API with the smallest result
# and not controllable from the customer.
self.client.locations.get_all()
except APIException:
except hcloud.APIException:
raise AnsibleError("Invalid Hetzner Cloud API Token.")

def _get_servers(self):
Expand All @@ -177,7 +175,7 @@ def _filter_servers(self):
self.network = self.client.networks.get_by_name(network)
if self.network is None:
self.network = self.client.networks.get_by_id(network)
except APIException:
except hcloud.APIException:
raise AnsibleError("The given network is not found.")

tmp = []
Expand Down Expand Up @@ -322,8 +320,10 @@ def verify_file(self, path):
def parse(self, inventory, loader, path, cache=True):
super().parse(inventory, loader, path, cache)

if not HAS_HCLOUD:
raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires hcloud-python.")
if not HAS_REQUESTS:
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._configure_hcloud_client()
Expand Down
19 changes: 14 additions & 5 deletions plugins/module_utils/hcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,31 @@

from ansible.module_utils.ansible_release import __version__
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:
import hcloud
import requests # pylint: disable=unused-import
except ImportError:
HAS_REQUESTS = False

HAS_HCLOUD = True
try:
import dateutil # pylint: disable=unused-import
except ImportError:
HAS_HCLOUD = False
HAS_DATEUTIL = False


class Hcloud:
def __init__(self, module, represent):
self.module = module
self.represent = represent
self.result = {"changed": False, self.represent: None}
if not HAS_HCLOUD:
module.fail_json(msg=missing_required_lib("hcloud-python"))
if not HAS_REQUESTS:
module.fail_json(msg=missing_required_lib("requests"))
if not HAS_DATEUTIL:
module.fail_json(msg=missing_required_lib("python-dateutil"))
self._build_client()

def _build_client(self):
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions plugins/module_utils/vendor/hcloud/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from ._exceptions import APIException, HCloudException # noqa
from .hcloud import Client # noqa
14 changes: 14 additions & 0 deletions plugins/module_utils/vendor/hcloud/_exceptions.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions plugins/module_utils/vendor/hcloud/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VERSION = "1.24.0" # x-release-please-version
Empty file.
90 changes: 90 additions & 0 deletions plugins/module_utils/vendor/hcloud/actions/client.py
Original file line number Diff line number Diff line change
@@ -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)
75 changes: 75 additions & 0 deletions plugins/module_utils/vendor/hcloud/actions/domain.py
Original file line number Diff line number Diff line change
@@ -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"""
Empty file.
Loading

0 comments on commit 8c7cd1e

Please sign in to comment.