From 90139f5c051421f2c14797ab31a005877d172e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20L=C3=B6sche?= Date: Sat, 14 Sep 2024 22:30:18 +0200 Subject: [PATCH] [hetzner][feat] Initial Hetzner Cloud support (#2168) --- .github/workflows/basecheck.yml | 4 + .github/workflows/check_pr_fixlib.yml | 4 + .github/workflows/check_pr_fixmetrics.yml | 4 + .github/workflows/check_pr_fixshell.yml | 4 + .github/workflows/check_pr_fixworker.yml | 4 + .github/workflows/check_pr_plugin_aws.yml | 4 + .github/workflows/check_pr_plugin_azure.yml | 4 + .../check_pr_plugin_digitalocean.yml | 4 + .../workflows/check_pr_plugin_dockerhub.yml | 4 + .../check_pr_plugin_example_collector.yml | 4 + .github/workflows/check_pr_plugin_gcp.yml | 4 + .github/workflows/check_pr_plugin_github.yml | 4 + .github/workflows/check_pr_plugin_hetzner.yml | 75 ++++ .github/workflows/check_pr_plugin_k8s.yml | 4 + .../workflows/check_pr_plugin_onelogin.yml | 4 + .github/workflows/check_pr_plugin_onprem.yml | 4 + .github/workflows/check_pr_plugin_posthog.yml | 4 + .github/workflows/check_pr_plugin_random.yml | 4 + .github/workflows/check_pr_plugin_scarf.yml | 4 + .github/workflows/check_pr_plugin_slack.yml | 4 + .github/workflows/check_pr_plugin_vsphere.yml | 4 + .github/workflows/codeql-analysis.yml | 4 + .github/workflows/create_plugin_workflows.py | 4 + .github/workflows/docker-build.yml | 4 + .github/workflows/fixcore_coverage.yml | 4 + .github/workflows/fixcore_lint.yml | 4 + .github/workflows/fixcore_test_and_build.yml | 4 + .github/workflows/model_check.yml | 4 + .github/workflows/publish.yml | 2 +- fixlib/fixlib/baseresources.py | 11 + plugins/gcp/fix_plugin_gcp/__init__.py | 3 +- plugins/hetzner/MANIFEST.in | 1 + plugins/hetzner/README.md | 30 ++ .../hetzner/fix_plugin_hetzner/__init__.py | 60 +++ plugins/hetzner/fix_plugin_hetzner/client.py | 5 + .../hetzner/fix_plugin_hetzner/collector.py | 409 ++++++++++++++++++ plugins/hetzner/fix_plugin_hetzner/config.py | 18 + .../hetzner/fix_plugin_hetzner/resources.py | 237 ++++++++++ plugins/hetzner/pyproject.toml | 45 ++ plugins/hetzner/setup.cfg | 7 + plugins/hetzner/test/test_args.py | 8 + plugins/hetzner/test/test_config.py | 9 + plugins/hetzner/tox.ini | 29 ++ 43 files changed, 1054 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/check_pr_plugin_hetzner.yml create mode 100644 plugins/hetzner/MANIFEST.in create mode 100644 plugins/hetzner/README.md create mode 100644 plugins/hetzner/fix_plugin_hetzner/__init__.py create mode 100644 plugins/hetzner/fix_plugin_hetzner/client.py create mode 100644 plugins/hetzner/fix_plugin_hetzner/collector.py create mode 100644 plugins/hetzner/fix_plugin_hetzner/config.py create mode 100644 plugins/hetzner/fix_plugin_hetzner/resources.py create mode 100644 plugins/hetzner/pyproject.toml create mode 100644 plugins/hetzner/setup.cfg create mode 100644 plugins/hetzner/test/test_args.py create mode 100644 plugins/hetzner/test/test_config.py create mode 100644 plugins/hetzner/tox.ini diff --git a/.github/workflows/basecheck.yml b/.github/workflows/basecheck.yml index e7488afdc0..abda4c2679 100644 --- a/.github/workflows/basecheck.yml +++ b/.github/workflows/basecheck.yml @@ -10,6 +10,10 @@ on: - 'fixlib/**' - 'plugins/**' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: basecheck: name: "basecheck" diff --git a/.github/workflows/check_pr_fixlib.yml b/.github/workflows/check_pr_fixlib.yml index 01922f16c4..3e89abb13e 100644 --- a/.github/workflows/check_pr_fixlib.yml +++ b/.github/workflows/check_pr_fixlib.yml @@ -11,6 +11,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: fixlib: name: "fixlib" diff --git a/.github/workflows/check_pr_fixmetrics.yml b/.github/workflows/check_pr_fixmetrics.yml index ebf7c74fd5..aa741d9184 100644 --- a/.github/workflows/check_pr_fixmetrics.yml +++ b/.github/workflows/check_pr_fixmetrics.yml @@ -12,6 +12,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: fixmetrics: name: "fixmetrics" diff --git a/.github/workflows/check_pr_fixshell.yml b/.github/workflows/check_pr_fixshell.yml index 9993cb2bf5..697b852bba 100644 --- a/.github/workflows/check_pr_fixshell.yml +++ b/.github/workflows/check_pr_fixshell.yml @@ -13,6 +13,10 @@ on: - 'requirements-all.txt' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: fixshell: name: "fixshell" diff --git a/.github/workflows/check_pr_fixworker.yml b/.github/workflows/check_pr_fixworker.yml index cc700b04b5..984ebdfaee 100644 --- a/.github/workflows/check_pr_fixworker.yml +++ b/.github/workflows/check_pr_fixworker.yml @@ -12,6 +12,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: fixworker: name: "fixworker" diff --git a/.github/workflows/check_pr_plugin_aws.yml b/.github/workflows/check_pr_plugin_aws.yml index f15cedadb0..75ffeaff73 100644 --- a/.github/workflows/check_pr_plugin_aws.yml +++ b/.github/workflows/check_pr_plugin_aws.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: aws: name: "aws" diff --git a/.github/workflows/check_pr_plugin_azure.yml b/.github/workflows/check_pr_plugin_azure.yml index 39f6f697e1..ddee301e55 100644 --- a/.github/workflows/check_pr_plugin_azure.yml +++ b/.github/workflows/check_pr_plugin_azure.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: azure: name: "azure" diff --git a/.github/workflows/check_pr_plugin_digitalocean.yml b/.github/workflows/check_pr_plugin_digitalocean.yml index 9bfe0cbf71..701bafdc45 100644 --- a/.github/workflows/check_pr_plugin_digitalocean.yml +++ b/.github/workflows/check_pr_plugin_digitalocean.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: digitalocean: name: "digitalocean" diff --git a/.github/workflows/check_pr_plugin_dockerhub.yml b/.github/workflows/check_pr_plugin_dockerhub.yml index 34a87324d8..2fd52d0a89 100644 --- a/.github/workflows/check_pr_plugin_dockerhub.yml +++ b/.github/workflows/check_pr_plugin_dockerhub.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: dockerhub: name: "dockerhub" diff --git a/.github/workflows/check_pr_plugin_example_collector.yml b/.github/workflows/check_pr_plugin_example_collector.yml index b6f3a9e7c8..8834c3a2fc 100644 --- a/.github/workflows/check_pr_plugin_example_collector.yml +++ b/.github/workflows/check_pr_plugin_example_collector.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: example_collector: name: "example_collector" diff --git a/.github/workflows/check_pr_plugin_gcp.yml b/.github/workflows/check_pr_plugin_gcp.yml index b797cbde00..fbdbac9a49 100644 --- a/.github/workflows/check_pr_plugin_gcp.yml +++ b/.github/workflows/check_pr_plugin_gcp.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: gcp: name: "gcp" diff --git a/.github/workflows/check_pr_plugin_github.yml b/.github/workflows/check_pr_plugin_github.yml index 54f54af2d6..3af3b65d46 100644 --- a/.github/workflows/check_pr_plugin_github.yml +++ b/.github/workflows/check_pr_plugin_github.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: github: name: "github" diff --git a/.github/workflows/check_pr_plugin_hetzner.yml b/.github/workflows/check_pr_plugin_hetzner.yml new file mode 100644 index 0000000000..2dfd4cd9f1 --- /dev/null +++ b/.github/workflows/check_pr_plugin_hetzner.yml @@ -0,0 +1,75 @@ +# Note: this workflow is automatically generated via the `create_pr` script in the same folder. +# Please do not change the file, but the script! + +name: Check PR (Plugin hetzner) +on: + push: + tags: + - "*.*.*" + branches: + - main + pull_request: + paths: + - 'fixlib/**' + - 'plugins/hetzner/**' + - '.github/**' + - 'requirements-all.txt' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + hetzner: + name: "hetzner" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + architecture: 'x64' + + - name: Restore dependency cache + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{runner.os}}-pip-${{hashFiles('./plugins/hetzner/pyproject.toml')}} + restore-keys: | + ${{runner.os}}-pip- + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade --editable fixlib/ + pip install tox wheel flake8 build + + - name: Run tests + working-directory: ./plugins/hetzner + run: tox + + - name: Archive code coverage results + uses: actions/upload-artifact@v4 + with: + name: plugin-hetzner-code-coverage-report + path: ./plugins/hetzner/htmlcov/ + + - name: Build a binary wheel and a source tarball + working-directory: ./plugins/hetzner + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + + - name: Publish distribution to PyPI + if: github.ref_type == 'tag' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_FIXINVENTORY_PLUGIN_HETZNER }} + packages_dir: ./plugins/hetzner/dist/ diff --git a/.github/workflows/check_pr_plugin_k8s.yml b/.github/workflows/check_pr_plugin_k8s.yml index 8c9d8cb09a..5599a12b59 100644 --- a/.github/workflows/check_pr_plugin_k8s.yml +++ b/.github/workflows/check_pr_plugin_k8s.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: k8s: name: "k8s" diff --git a/.github/workflows/check_pr_plugin_onelogin.yml b/.github/workflows/check_pr_plugin_onelogin.yml index e862bedfd6..5f19d7ba5b 100644 --- a/.github/workflows/check_pr_plugin_onelogin.yml +++ b/.github/workflows/check_pr_plugin_onelogin.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: onelogin: name: "onelogin" diff --git a/.github/workflows/check_pr_plugin_onprem.yml b/.github/workflows/check_pr_plugin_onprem.yml index f97ad794ce..5a23e86aa2 100644 --- a/.github/workflows/check_pr_plugin_onprem.yml +++ b/.github/workflows/check_pr_plugin_onprem.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: onprem: name: "onprem" diff --git a/.github/workflows/check_pr_plugin_posthog.yml b/.github/workflows/check_pr_plugin_posthog.yml index ad452faea3..bc4cbbbc4b 100644 --- a/.github/workflows/check_pr_plugin_posthog.yml +++ b/.github/workflows/check_pr_plugin_posthog.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: posthog: name: "posthog" diff --git a/.github/workflows/check_pr_plugin_random.yml b/.github/workflows/check_pr_plugin_random.yml index 2be2525c4a..e14cd6d474 100644 --- a/.github/workflows/check_pr_plugin_random.yml +++ b/.github/workflows/check_pr_plugin_random.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: random: name: "random" diff --git a/.github/workflows/check_pr_plugin_scarf.yml b/.github/workflows/check_pr_plugin_scarf.yml index 8dd3d107e9..a1ae7bef88 100644 --- a/.github/workflows/check_pr_plugin_scarf.yml +++ b/.github/workflows/check_pr_plugin_scarf.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: scarf: name: "scarf" diff --git a/.github/workflows/check_pr_plugin_slack.yml b/.github/workflows/check_pr_plugin_slack.yml index ce81d43af3..a27286987b 100644 --- a/.github/workflows/check_pr_plugin_slack.yml +++ b/.github/workflows/check_pr_plugin_slack.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: slack: name: "slack" diff --git a/.github/workflows/check_pr_plugin_vsphere.yml b/.github/workflows/check_pr_plugin_vsphere.yml index ef59159192..748185f40e 100644 --- a/.github/workflows/check_pr_plugin_vsphere.yml +++ b/.github/workflows/check_pr_plugin_vsphere.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: vsphere: name: "vsphere" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 249d40f6dd..23148ad2ec 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -8,6 +8,10 @@ on: schedule: - cron: '26 0 * * 1' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: analyze: name: Analyze diff --git a/.github/workflows/create_plugin_workflows.py b/.github/workflows/create_plugin_workflows.py index 76380cf1e7..4e60f75618 100755 --- a/.github/workflows/create_plugin_workflows.py +++ b/.github/workflows/create_plugin_workflows.py @@ -21,6 +21,10 @@ - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: @name@: name: "@name@" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index a4b34c35c5..83f95bf9b7 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -15,6 +15,10 @@ on: - "requirements-all.txt" workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: split-build: name: "Build split Docker images" # Do not rename without updating workflow defined in publish.yml diff --git a/.github/workflows/fixcore_coverage.yml b/.github/workflows/fixcore_coverage.yml index da3771b574..80cab4ad17 100644 --- a/.github/workflows/fixcore_coverage.yml +++ b/.github/workflows/fixcore_coverage.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: fixcore-coverage: name: "Coverage (fixcore)" diff --git a/.github/workflows/fixcore_lint.yml b/.github/workflows/fixcore_lint.yml index 8ca995e394..0594df6753 100644 --- a/.github/workflows/fixcore_lint.yml +++ b/.github/workflows/fixcore_lint.yml @@ -13,6 +13,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: fixcore-lint: name: "Lint (fixcore)" diff --git a/.github/workflows/fixcore_test_and_build.yml b/.github/workflows/fixcore_test_and_build.yml index edb6a019ad..63c70b2a5d 100644 --- a/.github/workflows/fixcore_test_and_build.yml +++ b/.github/workflows/fixcore_test_and_build.yml @@ -15,6 +15,10 @@ on: - '.github/**' - 'requirements-all.txt' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: fixcore-test-and-build: name: "Test and build (fixcore)" diff --git a/.github/workflows/model_check.yml b/.github/workflows/model_check.yml index 0d1bbd84ab..ea6fcf1429 100644 --- a/.github/workflows/model_check.yml +++ b/.github/workflows/model_check.yml @@ -22,6 +22,10 @@ on: - 'requirements-all.txt' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: model: name: "model" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b1f5c02a09..0e26cdb001 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.run_id }} cancel-in-progress: true jobs: diff --git a/fixlib/fixlib/baseresources.py b/fixlib/fixlib/baseresources.py index 7d888d1e4f..a6eb17f191 100644 --- a/fixlib/fixlib/baseresources.py +++ b/fixlib/fixlib/baseresources.py @@ -894,6 +894,8 @@ class BaseZone(PhantomBaseResource): kind_description: ClassVar[str] = "A zone." metadata: ClassVar[Dict[str, Any]] = {"icon": "zone", "group": "control"} + long_name: Optional[str] = None + def zone(self, graph: Optional[Any] = None) -> BaseZone: return self @@ -1194,6 +1196,15 @@ class BaseRoutingTable(BaseResource): _categories: ClassVar[List[Category]] = [Category.networking] +@define(eq=False, slots=False) +class BaseRoute(BaseResource): + kind: ClassVar[str] = "route" + kind_display: ClassVar[str] = "Network Route" + kind_description: ClassVar[str] = "A network route." + metadata: ClassVar[Dict[str, Any]] = {"icon": "route", "group": "networking"} + _categories: ClassVar[List[Category]] = [Category.networking] + + @define(eq=False, slots=False) class BaseNetworkAcl(BaseResource): kind: ClassVar[str] = "network_acl" diff --git a/plugins/gcp/fix_plugin_gcp/__init__.py b/plugins/gcp/fix_plugin_gcp/__init__.py index 8b6a3b95e4..88ef1b6558 100644 --- a/plugins/gcp/fix_plugin_gcp/__init__.py +++ b/plugins/gcp/fix_plugin_gcp/__init__.py @@ -3,8 +3,7 @@ from typing import Optional, Dict, Any import fixlib.proc -from fixlib.args import ArgumentParser -from fixlib.args import Namespace +from fixlib.args import ArgumentParser, Namespace from fixlib.baseplugin import BaseCollectorPlugin from fixlib.baseresources import Cloud from fixlib.config import Config, RunningConfig diff --git a/plugins/hetzner/MANIFEST.in b/plugins/hetzner/MANIFEST.in new file mode 100644 index 0000000000..bb3ec5f0d4 --- /dev/null +++ b/plugins/hetzner/MANIFEST.in @@ -0,0 +1 @@ +include README.md diff --git a/plugins/hetzner/README.md b/plugins/hetzner/README.md new file mode 100644 index 0000000000..6cfcea76bc --- /dev/null +++ b/plugins/hetzner/README.md @@ -0,0 +1,30 @@ +# fix-plugin-hetzner + +Hetzner Collector Plugin for Fix (alpha) + +This collector is in alpha stage and may not work as expected. Use at your own risk. + +- [x] Resource collection +- [ ] Resource deletion +- [ ] Tag update + +## Configuration + +Hetzner has no API to introspect a token, so you need to manually maintain the project name associated with an API token. Provide names in the same order as the corresponding API tokens. + +``` +fixworker: + collector: + - 'hetzner' +hetzner: + hcloud_project_names: + - 'dev' + - 'global' + hcloud_tokens: + - '0ytCtPtcyUO1fEwLIYarEQaiY04E9P9tDIowK1JD8mX5K5jsLhPmiwkMLLuDGMxG' + - 'nt71Kl3pSscVt5Mey1NUfERXeaxHyDru988De7UA4ew48eAvMMsQ8tserBEOwLXq' +``` + +## License + +See [LICENSE](../../LICENSE) for details. diff --git a/plugins/hetzner/fix_plugin_hetzner/__init__.py b/plugins/hetzner/fix_plugin_hetzner/__init__.py new file mode 100644 index 0000000000..08a27165d5 --- /dev/null +++ b/plugins/hetzner/fix_plugin_hetzner/__init__.py @@ -0,0 +1,60 @@ +from fixlib.baseplugin import BaseCollectorPlugin +from fixlib.args import ArgumentParser +from fixlib.config import Config +from fixlib.core.actions import CoreFeedback +from fixlib.graph import MaxNodesExceeded +from fixlib.baseresources import Cloud +from fixlib.logger import log +from typing import Any, Optional +from .config import HetznerConfig +from .collector import HcloudCollector +from .resources import HcloudProject + + +class HetznerCollectorPlugin(BaseCollectorPlugin): + cloud = "hetzner" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.core_feedback: Optional[CoreFeedback] = None + + def collect(self) -> None: + assert self.core_feedback, "core_feedback is not set" + if len(Config.hetzner.hcloud_tokens) != len(Config.hetzner.hcloud_project_names): + log.error("The number of tokens and project names must be the same!") + return + self.collect_hcloud() + + def collect_hcloud(self) -> None: + log.debug("plugin: collecting Hetzner Cloud resources") + feedback = self.core_feedback.with_context("hetzner") + cloud = Cloud(id=self.cloud, name="Hetzner") + + for i, api_token in enumerate(Config.hetzner.hcloud_tokens): + project = HcloudProject(id=Config.hetzner.hcloud_project_names[i]) + self.core_feedback.progress_done(project.id, 0, 1) + collector = HcloudCollector(cloud, project, api_token, feedback, self.max_resources_per_account) + try: + collector.collect() + self.send_account_graph(collector.graph) + except MaxNodesExceeded: + log.error(f"Max nodes exceeded, stopping collection in {project.kdname}") + continue + except Exception as e: + log.error(f"Error collecting resources in {project.kdname}: {e}") + continue + finally: + self.core_feedback.progress_done(project.id, 1, 1) + + @staticmethod + def add_args(arg_parser: ArgumentParser) -> None: + pass + + @staticmethod + def add_config(config: Config) -> None: + """Add any plugin config to the global config store. + + Method called by the PluginLoader upon plugin initialization. + Can be used to introduce plugin config arguments to the global config store. + """ + config.add_config(HetznerConfig) diff --git a/plugins/hetzner/fix_plugin_hetzner/client.py b/plugins/hetzner/fix_plugin_hetzner/client.py new file mode 100644 index 0000000000..d5daf25853 --- /dev/null +++ b/plugins/hetzner/fix_plugin_hetzner/client.py @@ -0,0 +1,5 @@ +from hcloud import Client + + +def get_client(api_token: str) -> Client: + return Client(token=api_token) diff --git a/plugins/hetzner/fix_plugin_hetzner/collector.py b/plugins/hetzner/fix_plugin_hetzner/collector.py new file mode 100644 index 0000000000..9b9265d27f --- /dev/null +++ b/plugins/hetzner/fix_plugin_hetzner/collector.py @@ -0,0 +1,409 @@ +from typing import Optional +from fixlib.logger import log +from fixlib.core.actions import CoreFeedback +from fixlib.baseresources import Cloud, VolumeStatus +from fixlib.graph import Graph +from .resources import ( + HcloudProject, + HcloudLocation, + HcloudDatacenter, + HcloudServer, + HcloudVolume, + HcloudNetwork, + HcloudSubnet, + HcloudRoute, + HcloudIso, + HcloudDeprecationInfo, + HcloudServerType, + HcloudFloatingIP, + HcloudPrimaryIP, + HcloudPrivateNetwork, + HcloudPublicNetwork, + HcloudIPv4Address, + HcloudIPv6Network, + HcloudFirewall, + HcloudFirewallRule, +) +from .client import get_client + + +class HcloudCollector: + def __init__( + self, + cloud: Cloud, + project: HcloudProject, + api_token: str, + core_feedback: CoreFeedback, + max_resources_per_account: Optional[int] = None, + ) -> None: + self.cloud = cloud + self.core_feedback = core_feedback + self.api_token = api_token + self.project = project + self.api_token = api_token + self.graph = Graph(root=self.project, max_nodes=max_resources_per_account) + + def collect(self): + log.info(f"Collecting resources in {self.project.kdname}") + self.add_locations() + self.add_datacenters() + self.add_server_types() + self.add_isos() + self.add_primary_ips() + self.add_volumes() + self.add_servers() + self.add_networks() + self.add_floating_ips() + self.add_firewalls() + + def add_locations(self): + log.info(f"Collecting locations in {self.project.kdname}") + client = get_client(self.api_token) + for location in client.locations.get_all(): + hcl = HcloudLocation( + hcloud_id=location.id, + id=location.name, + name=location.name, + long_name=location.description, + latitude=location.latitude, + longitude=location.longitude, + cloud=self.cloud, + account=self.project, + ) + self.graph.add_resource(self.project, hcl) + + def add_datacenters(self): + log.info(f"Collecting datacenters in {self.project.kdname}") + client = get_client(self.api_token) + for datacenter in client.datacenters.get_all(): + hcl = self.graph.search_first_all({"kind": "hcloud_location", "id": datacenter.location.name}) + if not hcl: + log.error(f"Location {datacenter.location.name} not found for datacenter {datacenter.name}") + continue + d = HcloudDatacenter( + hcloud_id=datacenter.id, + id=datacenter.name, + name=datacenter.name, + long_name=datacenter.description, + cloud=self.cloud, + account=self.project, + region=hcl, + ) + self.graph.add_resource(hcl, d) + + def add_networks(self): + log.info(f"Collecting networks in {self.project.kdname}") + client = get_client(self.api_token) + for network in client.networks.get_all(): + hcn = HcloudNetwork( + hcloud_id=network.id, + id=network.name, + name=network.name, + tags=network.labels, + cloud=self.cloud, + account=self.project, + ip_range=network.ip_range, + network_subnets=[ + HcloudSubnet( + type=s.type, + ip_range=s.ip_range, + network_zone=s.network_zone, + gateway=s.gateway, + vswitch_id=s.vswitch_id, + ) + for s in network.subnets + ], + network_routes=[HcloudRoute(destination=r.destination, gateway=r.gateway) for r in network.routes], + expose_routes_to_vswitch=network.expose_routes_to_vswitch, + protection=network.protection, + ) + self.graph.add_resource(self.project, hcn) + for server in network.servers: + hcs = self.graph.search_first_all({"kind": "hcloud_server", "id": server.name}) + if not hcs: + log.error(f"Server {server.name} not found for network {network.name}") + continue + self.graph.add_edge(hcn, hcs) + + def add_volumes(self): + log.info(f"Collecting volumes in {self.project.kdname}") + client = get_client(self.api_token) + for volume in client.volumes.get_all(): + hcl = self.graph.search_first_all({"kind": "hcloud_location", "id": volume.location.name}) + if not hcl: + log.error(f"Location {volume.location.name} not found for volume {volume.name}") + continue + + volume_status = VolumeStatus.UNKNOWN + if volume.status == "available": + if volume.server is None: + volume_status = VolumeStatus.AVAILABLE + else: + volume_status = VolumeStatus.IN_USE + elif volume.status == "creating": + volume_status = VolumeStatus.BUSY + + hcv = HcloudVolume( + hcloud_id=volume.id, + id=volume.name, + name=volume.name, + tags=volume.labels, + ctime=volume.created, + volume_size=volume.size, + volume_status=volume_status, + linux_device=volume.linux_device, + protection=volume.protection, + format=volume.format, + cloud=self.cloud, + account=self.project, + region=hcl, + ) + self.graph.add_resource(hcl, hcv) + + def add_servers(self): + log.info(f"Collecting servers in {self.project.kdname}") + client = get_client(self.api_token) + for server in client.servers.get_all(): + hcd = self.graph.search_first_all({"kind": "hcloud_datacenter", "id": server.datacenter.name}) + if not hcd: + log.error(f"Datacenter {server.datacenter.name} not found for server {server.name}") + continue + hcl = hcd.region() + + public_net = None + instance_cores = None + instance_memory = None + instance_type = None + if server.public_net: + ipv4 = None + ipv6 = None + primary_ipv4 = None + primary_ipv6 = None + if server.public_net.ipv4: + ipv4 = HcloudIPv4Address( + ip_address=server.public_net.ipv4.ip, + blocked=server.public_net.ipv4.blocked, + dns_ptr=server.public_net.ipv4.dns_ptr, + ) + if server.public_net.ipv6: + ipv6 = HcloudIPv6Network( + ip_address=server.public_net.ipv6.ip, + blocked=server.public_net.ipv6.blocked, + dns_ptr=server.public_net.ipv6.dns_ptr, + network=server.public_net.ipv6.network, + network_mask=server.public_net.ipv6.network_mask, + ) + public_net = HcloudPublicNetwork( + ipv4=ipv4, + ipv6=ipv6, + ) + + private_net = None + if server.private_net: + for pnet in server.private_net: + pn = HcloudPrivateNetwork( + ip_address=pnet.ip, + alias_ips=pnet.alias_ips, + mac_address=pnet.mac_address, + ) + if not private_net: + private_net = [] + private_net.append(pn) + + st = None + if server.server_type: + st: HcloudServerType = self.graph.search_first_all( + {"kind": "hcloud_server_type", "id": server.server_type.name} + ) + if st: + instance_cores = st.instance_cores + instance_memory = st.instance_memory + instance_type = st.name + s = HcloudServer( + hcloud_id=server.id, + id=server.name, + name=server.name, + ctime=server.created, + tags=server.labels, + instance_cores=instance_cores, + instance_memory=instance_memory, + instance_type=instance_type, + public_net=public_net, + private_net=private_net, + rescue_enabled=server.rescue_enabled, + locked=server.locked, + backup_window=server.backup_window, + outgoing_traffic=server.outgoing_traffic, + ingoing_traffic=server.ingoing_traffic, + included_traffic=server.included_traffic, + primary_disk_size=server.primary_disk_size, + protection=server.protection, + cloud=self.cloud, + account=self.project, + region=hcl, + zone=hcd, + ) + self.graph.add_resource(hcl, s) + self.graph.add_edge(hcd, s) + if st: + self.graph.add_edge(st, s) + for volume in server.volumes: + v = self.graph.search_first_all({"kind": "hcloud_volume", "id": volume.name}) + if not v: + log.error(f"Volume {volume.name} not found for server {server.name}") + continue + self.graph.add_edge(s, v) + + if server.public_net: + if server.public_net.primary_ipv4: + primary_ipv4 = self.graph.search_first_all( + {"kind": "hcloud_primary_ip", "id": server.public_net.primary_ipv4.ip} + ) + if primary_ipv4: + self.graph.add_edge(s, primary_ipv4) + if server.public_net.primary_ipv6: + primary_ipv6 = self.graph.search_first_all( + {"kind": "hcloud_primary_ip", "id": server.public_net.primary_ipv6.ip} + ) + if primary_ipv6: + self.graph.add_edge(s, primary_ipv6) + + def add_isos(self): + log.info(f"Collecting ISOs in {self.project.kdname}") + client = get_client(self.api_token) + for iso in client.isos.get_all(): + deprecation = None + if iso.deprecation: + deprecation = HcloudDeprecationInfo( + announced_at=iso.deprecation.announced, + unavailable_after=iso.deprecation.unavailable_after, + ) + i = HcloudIso( + hcloud_id=iso.id, + id=iso.name, + name=iso.name, + cloud=self.cloud, + account=self.project, + description=iso.description, + type=iso.type, + deprecated_at=iso.deprecated, + deprecation=deprecation, + ) + self.graph.add_resource(self.project, i) + + def add_server_types(self): + log.info(f"Collecting server types in {self.project.kdname}") + client = get_client(self.api_token) + for server_type in client.server_types.get_all(): + deprecation = None + if server_type.deprecation: + deprecation = HcloudDeprecationInfo( + announced_at=server_type.deprecation.announced, + unavailable_after=server_type.deprecation.unavailable_after, + ) + st = HcloudServerType( + hcloud_id=server_type.id, + id=server_type.name, + name=server_type.name, + cloud=self.cloud, + account=self.project, + description=server_type.description, + instance_cores=server_type.cores, + instance_memory=server_type.memory, + volume_size=server_type.disk, + prices=server_type.prices, + storage_type=server_type.storage_type, + cpu_type=server_type.cpu_type, + architecture=server_type.architecture, + deprecated=server_type.deprecated, + deprecation=deprecation, + ) + self.graph.add_resource(self.project, st) + + def add_floating_ips(self): + log.info(f"Collecting floating IPs in {self.project.kdname}") + client = get_client(self.api_token) + for floating_ip in client.floating_ips.get_all(): + hcl = self.graph.search_first_all({"kind": "hcloud_location", "id": floating_ip.home_location.name}) + fi = HcloudFloatingIP( + hcloud_id=floating_ip.id, + id=floating_ip.ip, + name=floating_ip.name, + tags=floating_ip.labels, + ctime=floating_ip.created, + cloud=self.cloud, + account=self.project, + region=hcl, + description=floating_ip.description, + ip_address=floating_ip.ip, + ip_address_family=floating_ip.type, + blocked=floating_ip.blocked, + dns_ptr=floating_ip.dns_ptr, + protection=floating_ip.protection, + ) + self.graph.add_resource(hcl, fi) + if floating_ip.server: + s = self.graph.search_first_all({"kind": "hcloud_server", "id": floating_ip.server.name}) + if s: + self.graph.add_edge(s, fi) + + def add_primary_ips(self): + log.info(f"Collecting primary IPs in {self.project.kdname}") + client = get_client(self.api_token) + for primary_ip in client.primary_ips.get_all(): + d = self.graph.search_first_all({"kind": "hcloud_datacenter", "id": primary_ip.datacenter.name}) + r = d.region() + p = HcloudPrimaryIP( + hcloud_id=primary_ip.id, + id=primary_ip.ip, + name=primary_ip.name, + tags=primary_ip.labels, + ctime=primary_ip.created, + cloud=self.cloud, + account=self.project, + region=r, + zone=d, + ip_address=primary_ip.ip, + ip_address_family=primary_ip.type, + blocked=primary_ip.blocked, + dns_ptr=primary_ip.dns_ptr, + assignee_id=primary_ip.assignee_id, + assigneer_type=primary_ip.assignee_type, + auto_delete=primary_ip.auto_delete, + protection=primary_ip.protection, + ) + self.graph.add_resource(r, p) + + def add_firewalls(self): + log.info(f"Collecting firewalls in {self.project.kdname}") + client = get_client(self.api_token) + for firewall in client.firewalls.get_all(): + if firewall.rules: + firewall_rules = [ + HcloudFirewallRule( + direction=r.direction, + port=r.port, + protocol=r.protocol, + source_ips=r.source_ips, + destination_ips=r.destination_ips, + description=r.description, + ) + for r in firewall.rules + ] + f = HcloudFirewall( + hcloud_id=firewall.id, + id=firewall.name, + name=firewall.name, + tags=firewall.labels, + ctime=firewall.created, + cloud=self.cloud, + account=self.project, + firewall_rules=firewall_rules, + ) + self.graph.add_resource(self.project, f) + if firewall.applied_to: + for firewall_resource in firewall.applied_to: + if firewall_resource.type == "server": + s = self.graph.search_first_all({"kind": "hcloud_server", "id": firewall_resource.server.name}) + if s: + self.graph.add_edge(f, s) diff --git a/plugins/hetzner/fix_plugin_hetzner/config.py b/plugins/hetzner/fix_plugin_hetzner/config.py new file mode 100644 index 0000000000..5f749d4ca6 --- /dev/null +++ b/plugins/hetzner/fix_plugin_hetzner/config.py @@ -0,0 +1,18 @@ +from attrs import define, field +from typing import ClassVar, List + + +@define +class HetznerConfig: + kind: ClassVar[str] = "hetzner" + hcloud_project_names: List[str] = field( + factory=list, + metadata={ + "description": ( + "Hetzner Cloud project names - Hetzner has no API to introspect a token, so you need" + " to manually maintain the project name associated with an API token. Provide names" + " in the same order as the corresponding API tokens." + ) + }, + ) + hcloud_tokens: List[str] = field(factory=list, metadata={"description": "Hetzner Cloud project API tokens"}) diff --git a/plugins/hetzner/fix_plugin_hetzner/resources.py b/plugins/hetzner/fix_plugin_hetzner/resources.py new file mode 100644 index 0000000000..dbd4514044 --- /dev/null +++ b/plugins/hetzner/fix_plugin_hetzner/resources.py @@ -0,0 +1,237 @@ +from attrs import define +from typing import ClassVar, Optional, Union +from datetime import datetime +from fixlib.graph import Graph +from fixlib.baseresources import ( + BaseAccount, + BaseRegion, + BaseZone, + BaseInstance, + BaseNetwork, + BaseResource, + BaseVolume, + BaseInstanceType, + BaseIPAddress, + BaseFirewall, +) + + +@define(eq=False, slots=False) +class HcloudResource(BaseResource): + kind: ClassVar[str] = "hcloud_resource" + kind_display: ClassVar[str] = "Hetzner Cloud Resource" + kind_description: ClassVar[str] = "A Hetzner Cloud Resource represents a single resource in the Hetzner Cloud" + + hcloud_id: Optional[int] = None + + def delete(self, graph: Graph) -> bool: + return NotImplemented + + def update_tag(self, key, value) -> bool: + return NotImplemented + + def delete_tag(self, key) -> bool: + return NotImplemented + + +@define(eq=False, slots=False) +class HcloudProject(BaseAccount, HcloudResource): + kind: ClassVar[str] = "hcloud_project" + + +@define(eq=False, slots=False) +class HcloudLocation(BaseRegion, HcloudResource): + kind: ClassVar[str] = "hcloud_location" + + +@define(eq=False, slots=False) +class HcloudDatacenter(BaseZone, HcloudResource): + kind: ClassVar[str] = "hcloud_datacenter" + + +@define(eq=False, slots=False) +class HcloudIPv4Address: + kind: ClassVar[str] = "hcloud_ipv4_address" + + ip_address: Optional[str] = None + blocked: Optional[bool] = None + dns_ptr: Optional[Union[str, list[dict[str, str]]]] = None + + +@define(eq=False, slots=False) +class HcloudIPv6Network: + kind: ClassVar[str] = "hcloud_ipv6_network" + + ip_address: Optional[str] = None + blocked: Optional[bool] = None + dns_ptr: Optional[Union[str, list[dict[str, str]]]] = None + network: Optional[str] = None + network_mask: Optional[str] = None + + +@define(eq=False, slots=False) +class HcloudFloatingIP(BaseIPAddress, HcloudResource): + kind: ClassVar[str] = "hcloud_floating_ip" + + description: Optional[str] = None + dns_ptr: Optional[Union[str, list[dict[str, str]]]] = None + home_location: Optional[HcloudLocation] = None + blocked: Optional[bool] = None + protection: Optional[dict[str, bool]] = None + + +@define(eq=False, slots=False) +class HcloudFirewallRule: + kind: ClassVar[str] = "hcloud_firewall_rule" + + direction: Optional[str] = None + port: Optional[str] = None + protocol: Optional[str] = None + source_ips: Optional[list[str]] = None + destination_ips: Optional[list[str]] = None + description: Optional[str] = None + + +@define(eq=False, slots=False) +class HcloudFirewall(BaseFirewall, HcloudResource): + kind: ClassVar[str] = "hcloud_firewall" + + firewall_rules: Optional[list[HcloudFirewallRule]] = None + + +@define(eq=False, slots=False) +class HcloudPrimaryIP(BaseIPAddress, HcloudResource): + kind: ClassVar[str] = "hcloud_primary_ip" + + dns_ptr: Optional[Union[str, list[dict[str, str]]]] = None + blocked: Optional[bool] = None + protection: Optional[dict[str, bool]] = None + assignee_id: Optional[int] = None + assigneer_type: Optional[str] = None + auto_delete: Optional[bool] = None + + +@define(eq=False, slots=False) +class HcloudPublicNetwork: + kind: ClassVar[str] = "hcloud_public_network" + + ipv4: Optional[HcloudIPv4Address] = None + ipv6: Optional[HcloudIPv6Network] = None + + +@define(eq=False, slots=False) +class HcloudPrivateNetwork: + kind: ClassVar[str] = "hcloud_private_network" + + ip_address: Optional[str] = None + alias_ips: Optional[list[str]] = None + mac_address: Optional[str] = None + + +@define(eq=False, slots=False) +class HcloudDeprecationInfo: + kind: ClassVar[str] = "hcloud_deprecation_info" + + announced_at: Optional[datetime] = None + unavailable_after: Optional[datetime] = None + + +@define(eq=False, slots=False) +class HcloudServerType(BaseInstanceType, HcloudResource): + kind: ClassVar[str] = "hcloud_server_type" + + description: Optional[str] = None + volume_size: Optional[int] = None + prices: Optional[list[dict[str, Union[str, float, dict[str, Union[str, float]]]]]] = None + storage_type: Optional[str] = None + cpu_type: Optional[str] = None + architecture: Optional[str] = None + deprecated: Optional[bool] = None + deprecation: Optional[HcloudDeprecationInfo] = None + included_traffic: Optional[int] = None + + +@define(eq=False, slots=False) +class HcloudVolume(BaseVolume, HcloudResource): + kind: ClassVar[str] = "hcloud_volume" + + linux_device: Optional[str] = None + protection: Optional[dict[str, bool]] = None + format: Optional[str] = None + + +@define(eq=False, slots=False) +class HcloudSubnet: + kind: ClassVar[str] = "hcloud_subnet" + + type: Optional[str] = None + ip_range: Optional[str] = None + network_zone: Optional[str] = None + gateway: Optional[str] = None + vswitch_id: Optional[int] = None + + +@define(eq=False, slots=False) +class HcloudRoute: + kind: ClassVar[str] = "hcloud_route" + + destination: Optional[str] = None + gateway: Optional[str] = None + + +@define(eq=False, slots=False) +class HcloudNetwork(BaseNetwork, HcloudResource): + kind: ClassVar[str] = "hcloud_network" + + ip_range: Optional[str] = None + network_subnets: Optional[list[HcloudSubnet]] = None + network_routes: Optional[list[HcloudRoute]] = None + expose_routes_to_vswitch: Optional[bool] = None + protection: Optional[dict[str, bool]] = None + + +@define(eq=False, slots=False) +class HcloudServer(BaseInstance, HcloudResource): + kind: ClassVar[str] = "hcloud_server" + + rescue_enabled: Optional[bool] = None + locked: Optional[bool] = None + backup_window: Optional[str] = None + outgoing_traffic: Optional[int] = None + ingoing_traffic: Optional[int] = None + included_traffic: Optional[int] = None + primary_disk_size: Optional[int] = None + protection: Optional[dict[str, bool]] = None + public_net: Optional[HcloudPublicNetwork] = None + private_net: Optional[list[HcloudPrivateNetwork]] = None + + +@define(eq=False, slots=False) +class HcloudIso(HcloudResource): + kind: ClassVar[str] = "hcloud_iso" + + description: Optional[str] = None + type: Optional[str] = None + architecture: Optional[str] = None + deprecated_at: Optional[datetime] = None + deprecation: Optional[HcloudDeprecationInfo] = None + + +@define(eq=False, slots=False) +class HcloudImage(HcloudResource): + kind: ClassVar[str] = "hcloud_image" + + type: Optional[str] = None + status: Optional[str] = None + description: Optional[str] = None + image_size: Optional[float] = None + disk_size: Optional[float] = None + created_at: Optional[datetime] = None + created_from: Optional[HcloudServer] = None + bound_to: Optional[HcloudServer] = None + os_flavor: Optional[str] = None + os_version: Optional[str] = None + architecture: Optional[str] = None + rapid_deploy: Optional[bool] = None + protection: Optional[dict[str, bool]] = None + deprecated_at: Optional[datetime] = None diff --git a/plugins/hetzner/pyproject.toml b/plugins/hetzner/pyproject.toml new file mode 100644 index 0000000000..35ffae2a94 --- /dev/null +++ b/plugins/hetzner/pyproject.toml @@ -0,0 +1,45 @@ +[project] +name = "fixinventory-plugin-hetzner" +description = "Fix Hetzner Collector Plugin" +version = "4.1.0" +authors = [{name="Some Engineering Inc."}] +license = { text="AGPLv3" } +requires-python = ">=3.11" +classifiers = [ + # Current project status + "Development Status :: 4 - Beta", + # Audience + "Intended Audience :: System Administrators", + "Intended Audience :: Information Technology", + # License information + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + # Supported python versions + "Programming Language :: Python :: 3.11", + # Supported OS's + "Operating System :: POSIX :: Linux", + "Operating System :: Unix", + # Extra metadata + "Environment :: Console", + "Natural Language :: English", + "Topic :: Security", + "Topic :: Utilities", +] +readme = {file="README.md", content-type="text/markdown"} + +dependencies = [ + "fixinventorylib==4.1.0", + "hcloud==2.2.0", +] + +[project.entry-points."fix.plugins"] +hetzner_collector = "fix_plugin_hetzner:HetznerCollectorPlugin" + +[project.urls] +Documentation = "https://inventory.fix.security" +Source = "https://github.com/someengineering/fix/tree/main/plugins/hetzner" + +[build-system] +requires = ["setuptools>=67.8.0", "wheel>=0.40.0", "build>=0.10.0"] +build-backend = "setuptools.build_meta" + + diff --git a/plugins/hetzner/setup.cfg b/plugins/hetzner/setup.cfg new file mode 100644 index 0000000000..7ca6537935 --- /dev/null +++ b/plugins/hetzner/setup.cfg @@ -0,0 +1,7 @@ +[options] +packages = find: +include_package_data = True +zip_safe = False + +[aliases] +test=pytest diff --git a/plugins/hetzner/test/test_args.py b/plugins/hetzner/test/test_args.py new file mode 100644 index 0000000000..d69db7620b --- /dev/null +++ b/plugins/hetzner/test/test_args.py @@ -0,0 +1,8 @@ +from fixlib.args import get_arg_parser +from fix_plugin_hetzner import HetznerCollectorPlugin + + +def test_args(): + arg_parser = get_arg_parser() + HetznerCollectorPlugin.add_args(arg_parser) + arg_parser.parse_args() diff --git a/plugins/hetzner/test/test_config.py b/plugins/hetzner/test/test_config.py new file mode 100644 index 0000000000..59eb3ec6c5 --- /dev/null +++ b/plugins/hetzner/test/test_config.py @@ -0,0 +1,9 @@ +from fixlib.config import Config +from fix_plugin_hetzner import HetznerCollectorPlugin + + +def test_config(): + config = Config("dummy", "dummy") + HetznerCollectorPlugin.add_config(config) + Config.init_default_config() + assert Config.hetzner.hcloud_tokens == [] diff --git a/plugins/hetzner/tox.ini b/plugins/hetzner/tox.ini new file mode 100644 index 0000000000..c550b7abb6 --- /dev/null +++ b/plugins/hetzner/tox.ini @@ -0,0 +1,29 @@ +[tox] +env_list = syntax, tests, black + +[flake8] +max-line-length=120 +exclude = .git,.tox,__pycache__,.idea,.pytest_cache +ignore=F403, F405, E722, N806, N813, E266, W503, E203 + +[pytest] +addopts= --cov=fix_plugin_hetzner -rs -vv --cov-report html +testpaths= test + +[testenv] +usedevelop = true +deps = + --editable=file:///{toxinidir}/../../fixlib + -r../../requirements-all.txt +# until this is fixed: https://github.com/pypa/setuptools/issues/3518 +setenv = + SETUPTOOLS_ENABLE_FEATURES = legacy-editable + +[testenv:syntax] +commands = flake8 --verbose + +[testenv:tests] +commands= pytest + +[testenv:black] +commands = black --line-length 120 --check --diff --target-version py39 .