1
0
Fork 0
mirror of https://github.com/ansible-collections/hetzner.hcloud.git synced 2026-02-03 23:51:48 +00:00

feat: add support for Storage Boxes (#676)

##### SUMMARY

We collect all changes for the Storage Box support in this PR. It will
only be merged when everything is implemented through smaller pull
requests targeting the `storage-boxes` branch.

---------

Co-authored-by: Julian Tölle <julian.toelle@hetzner-cloud.de>
This commit is contained in:
Jonas L. 2025-12-10 13:18:36 +01:00 committed by GitHub
parent bc61715c92
commit 5394c6f246
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
109 changed files with 4305 additions and 4 deletions

View file

@ -0,0 +1,52 @@
minor_changes:
- storage_box - New module to create and manage Storage Boxes in Hetzner.
- storage_box_info - New module to gather infos about Hetzner Storage Boxes.
- storage_box_snapshot - New module to create and manage Storage Box Snapshots in Hetzner.
- storage_box_snapshot_info - New module to gather infos about Hetzner Storage Box Snapshots.
- storage_box_subaccount - New module to create and manage Storage Box Subaccounts in Hetzner.
- storage_box_subaccount_info - New module to gather infos about Hetzner Storage Box Subaccounts.
- storage_box_type_info - New module to gather infos about Hetzner Storage Box Types.
release_summary: |
This release adds support for the new `Storage Box API`_.
Storage Box support is **experimental**, breaking changes may occur within minor releases.
See the `experimental tracking issue`_ for more details.
**Examples**
.. code:: yaml
- name: Create a Storage Box
hetzner.hcloud.storage_box:
name: backups
storage_box_type: bx11
location: fsn1
password: my-secret
access_settings:
reachable_externally: true
ssh_enabled: true
state: present
- name: Create a Storage Box Subaccount
hetzner.hcloud.storage_box_subaccount:
storage_box: backups
name: subaccount1
home_directory: backups/subaccount1
password: secret
access_settings:
readonly: true
labels:
env: prod
state: present
- name: Take a Storage Box Snapshot
hetzner.hcloud.storage_box_snapshot:
storage_box: backups
description: before app migration
labels:
env: prod
state: present
.. _Storage Box API: https://docs.hetzner.cloud/reference/hetzner#storage-boxes
.. _experimental tracking issue: https://github.com/ansible-collections/hetzner.hcloud/issues/756

View file

@ -32,6 +32,13 @@ action_groups:
- server_type_info
- ssh_key
- ssh_key_info
- storage_box
- storage_box_info
- storage_box_snapshot
- storage_box_snapshot_info
- storage_box_subaccount
- storage_box_subaccount_info
- storage_box_type_info
- subnetwork
- volume
- volume_info
@ -103,6 +110,20 @@ plugin_routing:
redirect: hetzner.hcloud.ssh_key_info
hcloud_ssh_key:
redirect: hetzner.hcloud.ssh_key
hcloud_storage_box:
redirect: storage_box
hcloud_storage_box_info:
redirect: storage_box_info
hcloud_storage_box_snapshot:
redirect: storage_box_snapshot
hcloud_storage_box_snapshot_info:
redirect: storage_box_snapshot_info
hcloud_storage_box_subaccount:
redirect: storage_box_subaccount
hcloud_storage_box_subaccount_info:
redirect: storage_box_subaccount_info
hcloud_storage_box_type_info:
redirect: storage_box_type_info
hcloud_subnetwork:
redirect: hetzner.hcloud.subnetwork
hcloud_volume_info:

View file

@ -10,24 +10,30 @@ class ModuleDocFragment:
options:
api_token:
description:
- The API Token for the Hetzner Cloud.
- The token for the Hetzner Cloud API.
- You can also set this option by using the C(HCLOUD_TOKEN) environment variable.
required: True
type: str
api_endpoint:
description:
- The API Endpoint for the Hetzner Cloud.
- The endpoint for the Hetzner Cloud API.
- You can also set this option by using the C(HCLOUD_ENDPOINT) environment variable.
default: https://api.hetzner.cloud/v1
type: str
aliases: [endpoint]
api_endpoint_hetzner:
description:
- The endpoint for the Hetzner API.
- You can also set this option by using the C(HETZNER_ENDPOINT) environment variable.
default: https://api.hetzner.com/v1
type: str
requirements:
- python-dateutil >= 2.7.5
- requests >=2.20
seealso:
- name: Documentation for Hetzner Cloud API
description: Complete reference for the Hetzner Cloud API.
- name: Documentation for Hetzner APIs
description: Complete reference for the Hetzner APIs.
link: https://docs.hetzner.cloud
"""

View file

@ -32,3 +32,10 @@ def experimental_warning_function(product: str, maturity: str, url: str):
module.warn(message)
return fn
storage_box_experimental_warning = experimental_warning_function(
"Storage Box support",
"experimental",
"https://github.com/ansible-collections/hetzner.hcloud/issues/756",
)

View file

@ -103,6 +103,7 @@ class AnsibleHCloud:
self.client = Client(
token=self.module.params["api_token"],
api_endpoint=self.module.params["api_endpoint"],
api_endpoint_hetzner=self.module.params["api_endpoint_hetzner"],
application_name="ansible-module",
application_version=version,
# Total waiting time before timeout is > 117.0
@ -163,6 +164,11 @@ class AnsibleHCloud:
"default": "https://api.hetzner.cloud/v1",
"aliases": ["endpoint"],
},
"api_endpoint_hetzner": {
"type": "str",
"fallback": (env_fallback, ["HETZNER_ENDPOINT"]),
"default": "https://api.hetzner.com/v1",
},
}
def _prepare_result(self) -> dict[str, Any]:

View file

@ -0,0 +1,66 @@
from __future__ import annotations
from ..module_utils.client import client_resource_not_found
from ..module_utils.vendor.hcloud.storage_boxes import (
BoundStorageBox,
StorageBoxesClient,
)
def get(client: StorageBoxesClient, param: str | int) -> BoundStorageBox:
"""
Get a Bound Storage Box either by ID or name.
If the given parameter is an ID, return a partial Bound Storage Box to reduce the amount
of API requests.
"""
try:
return BoundStorageBox(
client,
data={"id": int(param)},
complete=False,
)
except ValueError: # param is not an id
result = client.get_by_name(param)
if result is None:
# pylint: disable=raise-missing-from
raise client_resource_not_found("storage box", param)
return result
def prepare_result(o: BoundStorageBox):
return {
"id": o.id,
"name": o.name,
"storage_box_type": o.storage_box_type.name,
"location": o.location.name,
"labels": o.labels,
"delete_protection": o.protection["delete"],
"access_settings": {
"reachable_externally": o.access_settings.reachable_externally,
"samba_enabled": o.access_settings.samba_enabled,
"ssh_enabled": o.access_settings.ssh_enabled,
"webdav_enabled": o.access_settings.webdav_enabled,
"zfs_enabled": o.access_settings.zfs_enabled,
},
"username": o.username,
"server": o.server,
"system": o.system,
"status": o.status,
"stats": {
"size": o.stats.size,
"size_data": o.stats.size_data,
"size_snapshots": o.stats.size_snapshots,
},
"snapshot_plan": (
None
if o.snapshot_plan is None
else {
"max_snapshots": o.snapshot_plan.max_snapshots,
"hour": o.snapshot_plan.hour,
"minute": o.snapshot_plan.minute,
"day_of_week": o.snapshot_plan.day_of_week,
"day_of_month": o.snapshot_plan.day_of_month,
}
),
}

View file

@ -0,0 +1,21 @@
from __future__ import annotations
from ..module_utils.vendor.hcloud.storage_boxes import (
BoundStorageBoxSnapshot,
)
def prepare_result(o: BoundStorageBoxSnapshot):
return {
"storage_box": o.storage_box.id,
"id": o.id,
"name": o.name,
"description": o.description,
"labels": o.labels,
"stats": {
"size": o.stats.size,
"size_filesystem": o.stats.size_filesystem,
},
"is_automatic": o.is_automatic,
"created": o.created.isoformat(),
}

View file

@ -0,0 +1,44 @@
from __future__ import annotations
from ..module_utils.vendor.hcloud.storage_boxes import (
BoundStorageBox,
BoundStorageBoxSubaccount,
)
NAME_LABEL_KEY = "ansible-name"
def get_by_name(storage_box: BoundStorageBox, name: str):
if not name:
raise ValueError(f"invalid storage box subaccount name: '{name}'")
result = storage_box.get_subaccount_list(
label_selector=f"{NAME_LABEL_KEY}={name}",
)
if len(result.subaccounts) == 0:
return None
if len(result.subaccounts) == 1:
return result.subaccounts[0]
raise ValueError(f"found multiple storage box subaccount with the same name: {name}")
def prepare_result(o: BoundStorageBoxSubaccount, name: str):
return {
"storage_box": o.storage_box.id,
"id": o.id,
"name": name,
"description": o.description,
"username": o.username,
"home_directory": o.home_directory,
"server": o.server,
"access_settings": {
"reachable_externally": o.access_settings.reachable_externally,
"samba_enabled": o.access_settings.samba_enabled,
"ssh_enabled": o.access_settings.ssh_enabled,
"webdav_enabled": o.access_settings.webdav_enabled,
"readonly": o.access_settings.readonly,
},
"labels": o.labels,
"created": o.created.isoformat(),
}

View file

@ -0,0 +1,623 @@
#!/usr/bin/python
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
---
module: storage_box
short_description: Create and manage Storage Boxes in Hetzner.
description:
- Create, update and delete Storage Boxes in Hetzner.
- See the L(Storage Boxes API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-boxes) for more details.
- B(Experimental:) Storage Box support is experimental, breaking changes may occur within minor releases.
See https://github.com/ansible-collections/hetzner.hcloud/issues/756 for more details.
author:
- Jonas Lammler (@jooola)
options:
id:
description:
- ID of the Storage Box to manage.
- Required if no Storage Box O(name) is given.
- If the ID is invalid, the module will fail.
type: int
name:
description:
- Name of the Storage Box to manage.
- Required if no Storage Box O(id) is given.
- Required if the Storage Box does not exist.
type: str
storage_box_type:
description:
- Name or ID of the Storage Box Type for the Storage Box.
- Required if the Storage Box does not exist.
type: str
aliases: [type]
location:
description:
- Name or ID of the Location for the Storage Box.
- Required if the Storage Box does not exist.
type: str
password:
description:
- Password for the Storage Box.
- Required if the Storage Box does not exist.
type: str
labels:
description:
- User-defined labels (key-value pairs) for the Storage Box.
type: dict
ssh_keys:
description:
- SSH public keys in OpenSSH format to inject into the Storage Box.
type: list
elements: str
access_settings:
description:
- Access settings of the Storage Box.
type: dict
suboptions:
reachable_externally:
description:
- Whether access from outside the Hetzner network is allowed.
type: bool
default: false
samba_enabled:
description:
- Whether the Samba subsystem is enabled.
type: bool
default: false
ssh_enabled:
description:
- Whether the SSH subsystem is enabled.
type: bool
default: false
webdav_enabled:
description:
- Whether the WebDAV subsystem is enabled.
type: bool
default: false
zfs_enabled:
description:
- Whether the ZFS snapshot folder is visible.
type: bool
default: false
snapshot_plan:
description:
- Snapshot plan of the Storage Box.
- Use null to disabled the snapshot plan.
type: dict
suboptions:
max_snapshots:
description:
- Maximum amount of Snapshots that will be created by this Snapshot Plan.
- Older Snapshots will be deleted.
type: int
required: true
hour:
description:
- Hour when the Snapshot Plan is executed (UTC).
type: int
required: true
minute:
description:
- Minute when the Snapshot Plan is executed (UTC).
type: int
required: true
day_of_week:
description:
- Day of the week when the Snapshot Plan is executed.
- Starts at 1 for Monday til 7 for Sunday.
- Null means every day.
type: int
day_of_month:
description:
- Day of the month when the Snapshot Plan is executed.
- Null means every day.
type: int
delete_protection:
description:
- Protect the Storage Box from deletion.
type: bool
snapshot:
description:
- Snapshot ID or Name to rollback to.
- Only used when O(state=rollback_snapshot)
type: str
state:
description:
- State of the Storage Box.
- C(reset_password) and C(rollback_snapshot) are not idempotent.
default: present
choices: [absent, present, reset_password, rollback_snapshot]
type: str
extends_documentation_fragment:
- hetzner.hcloud.hcloud
"""
EXAMPLES = """
- name: Create a Storage Box
hetzner.hcloud.storage_box:
name: my-storage-box
storage_box_type: bx11
location: fsn1
password: my-secret
labels:
env: prod
state: present
- name: Create a Storage Box with access settings
hetzner.hcloud.storage_box:
name: my-storage-box
storage_box_type: bx11
location: fsn1
password: my-secret
access_settings:
reachable_externally: true
ssh_enabled: true
samba_enabled: false
webdav_enabled: false
zfs_enabled: false
state: present
- name: Create a Storage Box with snapshot plan
hetzner.hcloud.storage_box:
name: my-storage-box
storage_box_type: bx11
location: fsn1
password: my-secret
snapshot_plan:
max_snapshots: 10
hour: 3
minute: 30
state: present
- name: Disable a Storage Box snapshot plan
hetzner.hcloud.storage_box:
name: my-storage-box
snapshot_plan: null
state: present
- name: Reset a Storage Box password
hetzner.hcloud.storage_box:
name: my-storage-box
password: my-secret
state: reset_password
- name: Rollback a Storage Box to a Snapshot
hetzner.hcloud.storage_box:
name: my-storage-box
snapshot: 2025-12-03T13-47-47
state: rollback_snapshot
- name: Delete a Storage Box
hetzner.hcloud.storage_box:
name: my-storage-box
state: absent
"""
RETURN = """
hcloud_storage_box:
description: Details about the Storage Box.
returned: always
type: dict
contains:
id:
description: ID of the Storage Box.
returned: always
type: int
sample: 1937415
name:
description: Name of the Storage Box.
returned: always
type: str
sample: my-storage-box
storage_box_type:
description: Name of the Storage Box Type.
returned: always
type: str
sample: bx11
location:
description: Name of the Location of the Storage Box.
returned: always
type: str
sample: fsn1
labels:
description: User-defined labels (key-value pairs) of the Storage Box.
returned: always
type: dict
sample:
env: prod
delete_protection:
description: Protect the Storage Box from deletion.
returned: always
type: bool
sample: false
access_settings:
description: Access settings of the Storage Box.
returned: always
type: dict
contains:
reachable_externally:
description: Whether access from outside the Hetzner network is allowed.
returned: always
type: bool
sample: false
samba_enabled:
description: Whether the Samba subsystem is enabled.
returned: always
type: bool
sample: false
ssh_enabled:
description: Whether the SSH subsystem is enabled.
returned: always
type: bool
sample: true
webdav_enabled:
description: Whether the WebDAV subsystem is enabled.
returned: always
type: bool
sample: false
zfs_enabled:
description: Whether the ZFS snapshot folder is visible.
returned: always
type: bool
sample: false
snapshot_plan:
description: Snapshot plan of the Storage Box.
returned: when enabled
type: dict
contains:
max_snapshots:
description: Maximum amount of Snapshots that will be created by this Snapshot Plan.
returned: always
type: int
sample: 10
hour:
description: Hour when the Snapshot Plan is executed (UTC).
returned: always
type: int
sample: 3
minute:
description: Minute when the Snapshot Plan is executed (UTC).
returned: always
type: int
sample: 30
day_of_week:
description: Day of the week when the Snapshot Plan is executed. Null means every day.
returned: always
type: int
sample: 1
day_of_month:
description: Day of the month when the Snapshot Plan is executed. Null means every day.
returned: always
type: int
sample: 30
username:
description: User name of the Storage Box.
returned: always
type: str
sample: u505337
server:
description: FQDN of the Storage Box.
returned: always
type: str
sample: u505337.your-storagebox.de
system:
description: Host system of the Storage Box.
returned: always
type: str
sample: HEL1-BX136
status:
description: Status of the Storage Box.
returned: always
type: str
sample: active
stats:
description: Statistics of the Storage Box.
returned: always
type: dict
contains:
size:
description: Current disk usage in bytes.
returned: always
type: int
sample: 10485760
size_data:
description: Current disk usage for data in bytes.
returned: always
type: int
sample: 10485760
size_snapshots:
description: Current disk usage for snapshots in bytes.
returned: always
type: int
sample: 10485760
"""
from ..module_utils import storage_box
from ..module_utils.client import client_resource_not_found
from ..module_utils.experimental import storage_box_experimental_warning
from ..module_utils.hcloud import AnsibleHCloud, AnsibleModule
from ..module_utils.vendor.hcloud import HCloudException
from ..module_utils.vendor.hcloud.locations import Location
from ..module_utils.vendor.hcloud.storage_box_types import StorageBoxType
from ..module_utils.vendor.hcloud.storage_boxes import (
BoundStorageBox,
StorageBoxAccessSettings,
StorageBoxSnapshot,
StorageBoxSnapshotPlan,
)
class AnsibleStorageBox(AnsibleHCloud):
represent = "storage_box"
storage_box: BoundStorageBox | None = None
def __init__(self, module: AnsibleModule):
storage_box_experimental_warning(module)
super().__init__(module)
def _prepare_result(self):
if self.storage_box is not None:
return storage_box.prepare_result(self.storage_box)
return {}
def _fetch(self):
if self.module.params.get("id") is not None:
self.storage_box = self.client.storage_boxes.get_by_id(self.module.params.get("id"))
else:
self.storage_box = self.client.storage_boxes.get_by_name(self.module.params.get("name"))
def _create(self):
self.fail_on_invalid_params(
required=["name", "storage_box_type", "location", "password"],
)
params = {
"name": self.module.params.get("name"),
"storage_box_type": StorageBoxType(self.module.params.get("storage_box_type")),
"location": Location(self.module.params.get("location")),
"password": self.module.params.get("password"),
}
if (value := self.module.params.get("labels")) is not None:
params["labels"] = value
if (value := self.module.params.get("access_settings")) is not None:
params["access_settings"] = StorageBoxAccessSettings.from_dict(value)
if (value := self.module.params.get("ssh_keys")) is not None:
params["ssh_keys"] = value
if not self.module.check_mode:
resp = self.client.storage_boxes.create(**params)
self.storage_box = resp.storage_box
resp.action.wait_until_finished()
if (value := self.module.params.get("delete_protection")) is not None:
if not self.module.check_mode:
action = self.storage_box.change_protection(delete=value)
action.wait_until_finished()
if self.module.param_is_defined("snapshot_plan"):
if (value := self.module.params.get("snapshot_plan")) is not None:
if not self.module.check_mode:
action = self.storage_box.enable_snapshot_plan(StorageBoxSnapshotPlan.from_dict(value))
action.wait_until_finished()
if not self.module.check_mode:
self.storage_box.reload()
self._mark_as_changed()
def _update(self):
need_reload = False
if (value := self.module.params.get("storage_box_type")) is not None:
if not self.storage_box.storage_box_type.has_id_or_name(value):
if not self.module.check_mode:
action = self.storage_box.change_type(StorageBoxType(value))
action.wait_until_finished()
need_reload = True
self._mark_as_changed()
if (value := self.module.params.get("access_settings")) is not None:
access_settings = StorageBoxAccessSettings.from_dict(value)
if self.storage_box.access_settings.to_payload() != access_settings.to_payload():
if not self.module.check_mode:
action = self.storage_box.update_access_settings(access_settings)
action.wait_until_finished()
need_reload = True
self._mark_as_changed()
if (value := self.module.params.get("delete_protection")) is not None:
if self.storage_box.protection["delete"] != value:
if not self.module.check_mode:
action = self.storage_box.change_protection(delete=value)
action.wait_until_finished()
need_reload = True
self._mark_as_changed()
if self.module.param_is_defined("snapshot_plan"):
if (value := self.module.params.get("snapshot_plan")) is not None:
snapshot_plan = StorageBoxSnapshotPlan.from_dict(value)
if (
self.storage_box.snapshot_plan is None
or self.storage_box.snapshot_plan.to_payload() != snapshot_plan.to_payload()
):
if not self.module.check_mode:
action = self.storage_box.enable_snapshot_plan(snapshot_plan)
action.wait_until_finished()
need_reload = True
self._mark_as_changed()
else:
if self.storage_box.snapshot_plan is not None:
if not self.module.check_mode:
action = self.storage_box.disable_snapshot_plan()
action.wait_until_finished()
need_reload = True
self._mark_as_changed()
params = {}
if (value := self.module.params.get("name")) is not None and value != self.storage_box.name:
self.fail_on_invalid_params(required=["id"])
params["name"] = value
self._mark_as_changed()
if (value := self.module.params.get("labels")) is not None and value != self.storage_box.labels:
params["labels"] = value
self._mark_as_changed()
# Update only if params holds changes or the data must be refreshed (actions
# were triggered)
if params or need_reload:
if not self.module.check_mode:
self.storage_box = self.storage_box.update(**params)
def _delete(self):
if not self.module.check_mode:
resp = self.storage_box.delete()
resp.action.wait_until_finished()
self.storage_box = None
self._mark_as_changed()
def present(self):
try:
self._fetch()
if self.storage_box is None:
self._create()
else:
self._update()
except HCloudException as exception:
self.fail_json_hcloud(exception)
def absent(self):
try:
self._fetch()
if self.storage_box is None:
return
self._delete()
except HCloudException as exception:
self.fail_json_hcloud(exception)
def reset_password(self):
self.fail_on_invalid_params(
required=["password"],
)
try:
self._fetch()
if self.storage_box is None:
raise client_resource_not_found(
"storage box",
self.module.params.get("id") or self.module.params.get("name"),
)
if not self.module.check_mode:
action = self.storage_box.reset_password(self.module.params.get("password"))
action.wait_until_finished()
self._mark_as_changed()
except HCloudException as exception:
self.fail_json_hcloud(exception)
def rollback_snapshot(self):
self.fail_on_invalid_params(
required=["snapshot"],
)
try:
self._fetch()
if self.storage_box is None:
raise client_resource_not_found(
"storage box",
self.module.params.get("id") or self.module.params.get("name"),
)
if not self.module.check_mode:
action = self.storage_box.rollback_snapshot(StorageBoxSnapshot(self.module.params.get("snapshot")))
action.wait_until_finished()
self._mark_as_changed()
except HCloudException as exception:
self.fail_json_hcloud(exception)
@classmethod
def define_module(cls):
return AnsibleModule(
argument_spec=dict(
id={"type": "int"},
name={"type": "str"},
storage_box_type={"type": "str", "aliases": ["type"]},
location={"type": "str"},
password={"type": "str", "no_log": True},
ssh_keys={"type": "list", "elements": "str", "no_log": False},
labels={"type": "dict"},
access_settings={
"type": "dict",
"options": dict(
reachable_externally={"type": "bool", "default": False},
samba_enabled={"type": "bool", "default": False},
ssh_enabled={"type": "bool", "default": False},
webdav_enabled={"type": "bool", "default": False},
zfs_enabled={"type": "bool", "default": False},
),
},
snapshot_plan={
"type": "dict",
"options": dict(
max_snapshots={"type": "int", "required": True},
hour={"type": "int", "required": True},
minute={"type": "int", "required": True},
day_of_week={"type": "int"},
day_of_month={"type": "int"},
),
},
delete_protection={"type": "bool"},
snapshot={"type": "str"},
state={
"choices": ["absent", "present", "reset_password", "rollback_snapshot"],
"default": "present",
},
**super().base_module_arguments(),
),
required_one_of=[["id", "name"]],
supports_check_mode=True,
)
def main():
module = AnsibleStorageBox.define_module()
o = AnsibleStorageBox(module)
match module.params.get("state"):
case "reset_password":
o.reset_password()
case "rollback_snapshot":
o.rollback_snapshot()
case "absent":
o.absent()
case _:
o.present()
result = o.get_result()
# Legacy return value naming pattern
result["hcloud_storage_box"] = result.pop(o.represent)
module.exit_json(**result)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,256 @@
#!/usr/bin/python
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
---
module: storage_box_info
short_description: Gather infos about Hetzner Storage Boxes.
description:
- Gather infos about Hetzner Storage Boxes.
- See the L(Storage Boxes API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-boxes) for more details.
- B(Experimental:) Storage Box support is experimental, breaking changes may occur within minor releases.
See https://github.com/ansible-collections/hetzner.hcloud/issues/756 for more details.
author:
- Jonas Lammler (@jooola)
options:
id:
description:
- ID of the Storage Box to get.
- If the ID is invalid, the module will fail.
type: int
name:
description:
- Name of the Storage Box to get.
type: str
label_selector:
description:
- Label selector to filter the Storage Boxes to get.
type: str
extends_documentation_fragment:
- hetzner.hcloud.hcloud
"""
EXAMPLES = """
- name: Gather all Storage Boxes
hetzner.hcloud.storage_box_info:
register: output
- name: Print the gathered infos
debug:
var: output.hcloud_storage_box_info
- name: Gather Storage Boxes by label
hetzner.hcloud.storage_box_info:
label_selector: env=prod
register: output
- name: Print the gathered infos
debug:
var: output.hcloud_storage_box_info
- name: Gather a Storage Box by name
hetzner.hcloud.storage_box_info:
name: backups
register: output
- name: Print the gathered infos
debug:
var: output.hcloud_storage_box_info[0]
- name: Gather a Storage Box by id
hetzner.hcloud.storage_box_info:
name: 12345
register: output
- name: Print the gathered infos
debug:
var: output.hcloud_storage_box_info[0]
"""
RETURN = """
hcloud_storage_box_info:
description: List of Storage Boxes.
returned: always
type: list
elements: dict
contains:
id:
description: ID of the Storage Box.
returned: always
type: int
sample: 1937415
name:
description: Name of the Storage Box.
returned: always
type: str
sample: my-storage-box
storage_box_type:
description: Name of the Storage Box Type.
returned: always
type: str
sample: bx11
location:
description: Name of the Location of the Storage Box.
returned: always
type: str
sample: fsn1
labels:
description: User-defined labels (key-value pairs) of the Storage Box.
returned: always
type: dict
sample:
env: prod
delete_protection:
description: Protect the Storage Box from deletion.
returned: always
type: bool
sample: false
access_settings:
description: Access settings of the Storage Box.
returned: always
type: dict
contains:
reachable_externally:
description: Whether access from outside the Hetzner network is allowed.
returned: always
type: bool
sample: false
samba_enabled:
description: Whether the Samba subsystem is enabled.
returned: always
type: bool
sample: false
ssh_enabled:
description: Whether the SSH subsystem is enabled.
returned: always
type: bool
sample: true
webdav_enabled:
description: Whether the WebDAV subsystem is enabled.
returned: always
type: bool
sample: false
zfs_enabled:
description: Whether the ZFS snapshot folder is visible.
returned: always
type: bool
sample: false
username:
description: User name of the Storage Box.
returned: always
type: str
sample: u505337
server:
description: FQDN of the Storage Box.
returned: always
type: str
sample: u505337.your-storagebox.de
system:
description: Host system of the Storage Box.
returned: always
type: str
sample: HEL1-BX136
status:
description: Status of the Storage Box.
returned: always
type: str
sample: active
stats:
description: Statistics of the Storage Box.
returned: always
type: dict
contains:
size:
description: Current disk usage in bytes.
returned: always
type: int
sample: 10485760
size_data:
description: Current disk usage for data in bytes.
returned: always
type: int
sample: 10485760
size_snapshots:
description: Current disk usage for snapshots in bytes.
returned: always
type: int
sample: 10485760
"""
from ansible.module_utils.basic import AnsibleModule
from ..module_utils import storage_box
from ..module_utils.experimental import storage_box_experimental_warning
from ..module_utils.hcloud import AnsibleHCloud
from ..module_utils.vendor.hcloud import HCloudException
from ..module_utils.vendor.hcloud.storage_boxes import (
BoundStorageBox,
)
class AnsibleStorageBox(AnsibleHCloud):
represent = "storage_box"
storage_box: list[BoundStorageBox] | None = None
def __init__(self, module: AnsibleModule):
storage_box_experimental_warning(module)
super().__init__(module)
def _prepare_result(self):
result = []
for o in self.storage_box or []:
if o is not None:
result.append(storage_box.prepare_result(o))
return result
def fetch(self):
try:
if (id_ := self.module.params.get("id")) is not None:
self.storage_box = [self.client.storage_boxes.get_by_id(id_)]
elif (name := self.module.params.get("name")) is not None:
self.storage_box = [self.client.storage_boxes.get_by_name(name)]
else:
params = {}
if (label_selector := self.module.params.get("label_selector")) is not None:
params["label_selector"] = label_selector
self.storage_box = self.client.storage_boxes.get_all(**params)
except HCloudException as exception:
self.fail_json_hcloud(exception)
@classmethod
def define_module(cls):
return AnsibleModule(
argument_spec=dict(
id={"type": "int"},
name={"type": "str"},
label_selector={"type": "str"},
**super().base_module_arguments(),
),
supports_check_mode=True,
)
def main():
module = AnsibleStorageBox.define_module()
o = AnsibleStorageBox(module)
o.fetch()
result = o.get_result()
# Legacy return value naming pattern
result["hcloud_storage_box_info"] = result.pop(o.represent)
module.exit_json(**result)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,282 @@
#!/usr/bin/python
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
---
module: storage_box_snapshot
short_description: Create and manage Storage Box Snapshots in Hetzner.
description:
- Create, update and delete Storage Box Snapshots in Hetzner.
- See the L(Storage Box Snapshots API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots) for more details.
- B(Experimental:) Storage Box support is experimental, breaking changes may occur within minor releases.
See https://github.com/ansible-collections/hetzner.hcloud/issues/756 for more details.
author:
- Jonas Lammler (@jooola)
options:
storage_box:
description:
- ID or Name of the parent Storage Box.
- Using the ID is preferred, to reduce the amount of API requests.
type: str
required: true
id:
description:
- ID of the Storage Box Snapshot to manage.
- Required when updating or deleting and if no Storage Box Snapshot O(name) is given.
- If the ID is invalid, the module will fail.
type: int
name:
description:
- Name of the Storage Box Snapshot to manage.
- Storage Box Snapshot names are defined by the API and cannot be changed.
- Required when updating or deleting and if no Storage Box Snapshot O(id) is given.
type: str
description:
description:
- Description of the Storage Box Snapshot.
type: str
labels:
description:
- User-defined labels (key-value pairs) for the Storage Box Snapshot.
type: dict
state:
description:
- State of the Storage Box Snapshot.
default: present
choices: [absent, present]
type: str
extends_documentation_fragment:
- hetzner.hcloud.hcloud
"""
EXAMPLES = """
- name: Create a Storage Box Snapshot
hetzner.hcloud.storage_box_snapshot:
storage_box: my-storage-box
description: before app migration
labels:
env: prod
state: present
- name: Delete a Storage Box Snapshot by name
hetzner.hcloud.storage_box_snapshot:
storage_box: my-storage-box
name: 2025-12-03T13-47-47
state: absent
- name: Delete a Storage Box Snapshot by id
hetzner.hcloud.storage_box_snapshot:
storage_box: 497436
id: 405920
state: absent
"""
RETURN = """
hcloud_storage_box_snapshot:
description: Details about the Storage Box Snapshot.
returned: always
type: dict
contains:
storage_box:
description: ID of the parent Storage Box.
returned: always
type: int
sample: 497436
id:
description: ID of the Storage Box Snapshot.
returned: always
type: int
sample: 405920
name:
description: Name of the Storage Box Snapshot.
returned: always
type: str
sample: 2025-02-12T11-35-19
description:
description: Description of the Storage Box Snapshot.
returned: always
type: str
sample: before app migration
labels:
description: User-defined labels (key-value pairs) of the Storage Box Snapshot.
returned: always
type: dict
sample:
env: prod
stats:
description: Statistics of the Storage Box Snapshot.
returned: always
type: dict
contains:
size:
description: Current storage requirements of the Snapshot in bytes.
returned: always
type: int
sample: 10485760
size_filesystem:
description: Size of the compressed file system contained in the Snapshot in bytes.
returned: always
type: int
sample: 10485760
is_automatic:
description: Whether the Storage Box Snapshot was created automatically.
returned: always
type: bool
sample: false
created:
description: Point in time when the Storage Box Snapshot was created (in RFC3339 format).
returned: always
type: str
sample: "2025-12-03T13:47:47Z"
"""
from ..module_utils import storage_box, storage_box_snapshot
from ..module_utils.experimental import storage_box_experimental_warning
from ..module_utils.hcloud import AnsibleHCloud, AnsibleModule
from ..module_utils.vendor.hcloud import HCloudException
from ..module_utils.vendor.hcloud.storage_boxes import (
BoundStorageBox,
BoundStorageBoxSnapshot,
)
class AnsibleStorageBoxSnapshot(AnsibleHCloud):
represent = "storage_box_snapshot"
storage_box: BoundStorageBox | None = None
storage_box_snapshot: BoundStorageBoxSnapshot | None = None
def __init__(self, module: AnsibleModule):
storage_box_experimental_warning(module)
super().__init__(module)
def _prepare_result(self):
if self.storage_box_snapshot is None:
return {}
return storage_box_snapshot.prepare_result(self.storage_box_snapshot)
def _fetch(self):
self.storage_box = storage_box.get(self.client.storage_boxes, self.module.params.get("storage_box"))
if (value := self.module.params.get("id")) is not None:
self.storage_box_snapshot = self.storage_box.get_snapshot_by_id(value)
elif (value := self.module.params.get("name")) is not None:
self.storage_box_snapshot = self.storage_box.get_snapshot_by_name(value)
def _create(self):
params = {}
if (value := self.module.params.get("description")) is not None:
params["description"] = value
if (value := self.module.params.get("labels")) is not None:
params["labels"] = value
if not self.module.check_mode:
resp = self.storage_box.create_snapshot(**params)
self.storage_box_snapshot = resp.snapshot
resp.action.wait_until_finished()
self.storage_box_snapshot.reload()
self._mark_as_changed()
def _update(self):
self.fail_on_invalid_params(
required_one_of=[["id", "name"]],
)
params = {}
if (value := self.module.params.get("description")) is not None:
if value != self.storage_box_snapshot.description:
params["description"] = value
self._mark_as_changed()
if (value := self.module.params.get("labels")) is not None:
if value != self.storage_box_snapshot.labels:
params["labels"] = value
self._mark_as_changed()
# Update only if params holds changes
if params:
if not self.module.check_mode:
self.storage_box_snapshot = self.storage_box_snapshot.update(**params)
def _delete(self):
if not self.module.check_mode:
resp = self.storage_box_snapshot.delete()
resp.action.wait_until_finished()
self.storage_box_snapshot = None
self._mark_as_changed()
def present(self):
try:
self._fetch()
if self.storage_box_snapshot is None:
self._create()
else:
self._update()
except HCloudException as exception:
self.fail_json_hcloud(exception)
def absent(self):
try:
self._fetch()
if self.storage_box_snapshot is None:
return
self._delete()
except HCloudException as exception:
self.fail_json_hcloud(exception)
@classmethod
def define_module(cls):
return AnsibleModule(
argument_spec=dict(
storage_box={"type": "str", "required": True},
id={"type": "int"},
name={"type": "str"},
description={"type": "str"},
labels={"type": "dict"},
state={
"choices": ["absent", "present"],
"default": "present",
},
**super().base_module_arguments(),
),
supports_check_mode=True,
)
def main():
module = AnsibleStorageBoxSnapshot.define_module()
o = AnsibleStorageBoxSnapshot(module)
match module.params.get("state"):
case "absent":
o.absent()
case _:
o.present()
result = o.get_result()
# Legacy return value naming pattern
result["hcloud_storage_box_snapshot"] = result.pop(o.represent)
module.exit_json(**result)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,213 @@
#!/usr/bin/python
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
---
module: storage_box_snapshot_info
short_description: Gather infos about Hetzner Storage Box Snapshots.
description:
- Gather infos about Hetzner Storage Box Snapshots.
- See the L(Storage Boxes API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-boxes) for more details.
- B(Experimental:) Storage Box support is experimental, breaking changes may occur within minor releases.
See https://github.com/ansible-collections/hetzner.hcloud/issues/756 for more details.
author:
- Jonas Lammler (@jooola)
options:
storage_box:
description:
- ID or Name of the parent Storage Box.
- Using the ID is preferred, to reduce the amount of API requests.
type: str
required: true
id:
description:
- ID of the Storage Box Snapshot to get.
- If the ID is invalid, the module will fail.
type: int
name:
description:
- Name of the Storage Box Snapshot to get.
type: str
label_selector:
description:
- Label selector to filter the Storage Box Snapshots to get.
type: str
extends_documentation_fragment:
- hetzner.hcloud.hcloud
"""
EXAMPLES = """
- name: Gather all Storage Box Snapshots
hetzner.hcloud.storage_box_snapshot_info:
storage_box: my-storage-box
register: output
- name: Gather Storage Box Snapshots by label
hetzner.hcloud.storage_box_snapshot_info:
storage_box: my-storage-box
label_selector: key=value
register: output
- name: Gather Storage Box Snapshot by id
hetzner.hcloud.storage_box_snapshot_info:
storage_box: 497436
id: 405920
register: output
- name: Gather Storage Box Snapshot by name
hetzner.hcloud.storage_box_snapshot_info:
storage_box: my-storage-box
name: 2025-02-12T11-35-19
register: output
- name: Print the gathered infos
debug:
var: output.hcloud_storage_box_snapshot_info
"""
RETURN = """
hcloud_storage_box_snapshot_info:
description: List of Storage Box Snapshots.
returned: always
type: list
contains:
storage_box:
description: ID of the parent Storage Box.
returned: always
type: int
sample: 497436
id:
description: ID of the Storage Box Snapshot.
returned: always
type: int
sample: 405920
name:
description: Name of the Storage Box Snapshot.
returned: always
type: str
sample: 2025-02-12T11-35-19
description:
description: Description of the Storage Box Snapshot.
returned: always
type: str
sample: before app migration
labels:
description: User-defined labels (key-value pairs) of the Storage Box Snapshot.
returned: always
type: dict
sample:
env: prod
stats:
description: Statistics of the Storage Box Snapshot.
returned: always
type: dict
contains:
size:
description: Current storage requirements of the Snapshot in bytes.
returned: always
type: int
sample: 10485760
size_filesystem:
description: Size of the compressed file system contained in the Snapshot in bytes.
returned: always
type: int
sample: 10485760
is_automatic:
description: Whether the Storage Box Snapshot was created automatically.
returned: always
type: bool
sample: false
created:
description: Point in time when the Storage Box Snapshot was created (in RFC3339 format).
returned: always
type: str
sample: "2025-12-03T13:47:47Z"
"""
from ..module_utils import storage_box, storage_box_snapshot
from ..module_utils.experimental import storage_box_experimental_warning
from ..module_utils.hcloud import AnsibleHCloud, AnsibleModule
from ..module_utils.vendor.hcloud import HCloudException
from ..module_utils.vendor.hcloud.storage_boxes import (
BoundStorageBox,
BoundStorageBoxSnapshot,
)
class AnsibleStorageBoxSnapshotInfo(AnsibleHCloud):
represent = "storage_box_snapshots"
storage_box: BoundStorageBox | None = None
storage_box_snapshots: list[BoundStorageBoxSnapshot] | None = None
def __init__(self, module: AnsibleModule):
storage_box_experimental_warning(module)
super().__init__(module)
def _prepare_result(self):
result = []
for o in self.storage_box_snapshots or []:
if o is not None:
result.append(storage_box_snapshot.prepare_result(o))
return result
def fetch(self):
try:
self.storage_box = storage_box.get(
self.client.storage_boxes,
self.module.params.get("storage_box"),
)
if (id_ := self.module.params.get("id")) is not None:
self.storage_box_snapshots = [self.storage_box.get_snapshot_by_id(id_)]
elif (name := self.module.params.get("name")) is not None:
self.storage_box_snapshots = [self.storage_box.get_snapshot_by_name(name)]
else:
params = {}
if (value := self.module.params.get("label_selector")) is not None:
params["label_selector"] = value
self.storage_box_snapshots = self.storage_box.get_snapshot_all(**params)
except HCloudException as exception:
self.fail_json_hcloud(exception)
@classmethod
def define_module(cls):
return AnsibleModule(
argument_spec=dict(
storage_box={"type": "str", "required": True},
id={"type": "int"},
name={"type": "str"},
label_selector={"type": "str"},
**super().base_module_arguments(),
),
supports_check_mode=True,
)
def main():
module = AnsibleStorageBoxSnapshotInfo.define_module()
o = AnsibleStorageBoxSnapshotInfo(module)
o.fetch()
result = o.get_result()
# Legacy return value naming pattern
result["hcloud_storage_box_snapshot_info"] = result.pop(o.represent)
module.exit_json(**result)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,476 @@
#!/usr/bin/python
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
---
module: storage_box_subaccount
short_description: Create and manage Storage Box Subaccounts in Hetzner.
description:
- Create, update and delete Storage Box Subaccounts in Hetzner.
- See the L(Storage Box Subaccounts API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts) for more details.
- B(Experimental:) Storage Box support is experimental, breaking changes may occur within minor releases.
See https://github.com/ansible-collections/hetzner.hcloud/issues/756 for more details.
author:
- Jonas Lammler (@jooola)
options:
storage_box:
description:
- ID or Name of the parent Storage Box.
- Using the ID is preferred, to reduce the amount of API requests.
type: str
required: true
id:
description:
- ID of the Storage Box Subaccount to manage.
- Required if no Storage Box Subaccount O(name) is given.
- If the ID is invalid, the module will fail.
type: int
name:
description:
- Name of the Storage Box Subaccount to manage.
- Required if no Storage Box Subaccount O(id) is given.
- Required if the Storage Box Subaccount does not exist.
- Because the API resource does not have this property, the name is stored
in the Storage Box Subaccount labels. This ensures that the module is
idempotent, and removes the need to use different module arguments for
create and update.
type: str
password:
description:
- Password for the Storage Box Subaccount.
- Required if the Storage Box Subaccount does not exist or when O(state=reset_password).
type: str
home_directory:
description:
- Home directory of the Storage Box Subaccount.
- Required if the Storage Box Subaccount does not exist.
type: str
access_settings:
description:
- Access settings of the Storage Box Subaccount.
type: dict
suboptions:
reachable_externally:
description:
- Whether access from outside the Hetzner network is allowed.
type: bool
default: false
samba_enabled:
description:
- Whether the Samba subsystem is enabled.
type: bool
default: false
ssh_enabled:
description:
- Whether the SSH subsystem is enabled.
type: bool
default: false
webdav_enabled:
description:
- Whether the WebDAV subsystem is enabled.
type: bool
default: false
readonly:
description:
- Whether the Subaccount is read-only.
type: bool
default: false
description:
description:
- Description of the Storage Box Subaccount.
type: str
labels:
description:
- User-defined labels (key-value pairs) for the Storage Box Subaccount.
type: dict
state:
description:
- State of the Storage Box Subaccount.
- C(reset_password) is not idempotent.
default: present
choices: [absent, present, reset_password]
type: str
extends_documentation_fragment:
- hetzner.hcloud.hcloud
"""
EXAMPLES = """
- name: Create a Storage Box Subaccount
hetzner.hcloud.storage_box_subaccount:
storage_box: my-storage-box
name: subaccount1
home_directory: backups/subaccount1
password: secret
access_settings:
reachable_externally: false
ssh_enabled: true
samba_enabled: false
webdav_enabled: false
readonly: false
labels:
env: prod
state: present
- name: Reset a Storage Box Subaccount password
hetzner.hcloud.storage_box_subaccount:
storage_box: my-storage-box
name: subaccount1
password: secret
state: reset_password
- name: Delete a Storage Box Subaccount by name
hetzner.hcloud.storage_box_subaccount:
storage_box: my-storage-box
name: subaccount1
state: absent
- name: Delete a Storage Box Subaccount by id
hetzner.hcloud.storage_box_subaccount:
storage_box: 497436
id: 158045
state: absent
"""
RETURN = """
hcloud_storage_box_subaccount:
description: Details about the Storage Box Subaccount.
returned: always
type: dict
contains:
storage_box:
description: ID of the parent Storage Box.
returned: always
type: int
sample: 497436
id:
description: ID of the Storage Box Subaccount.
returned: always
type: int
sample: 158045
name:
description: Name of the Storage Box Subaccount.
returned: always
type: str
sample: subaccount1
description:
description: Description of the Storage Box Subaccount.
returned: always
type: str
sample: backups from subaccount1
home_directory:
description: Home directory of the Storage Box Subaccount.
returned: always
type: str
sample: backups/subaccount1
username:
description: Username of the Storage Box Subaccount.
returned: always
type: str
sample: u514605-sub1
server:
description: FQDN of the Storage Box Subaccount.
returned: always
type: str
sample: u514605-sub1.your-storagebox.de
access_settings:
description: Access settings of the Storage Box Subaccount.
returned: always
type: dict
contains:
reachable_externally:
description: Whether access from outside the Hetzner network is allowed.
returned: always
type: bool
sample: false
samba_enabled:
description: Whether the Samba subsystem is enabled.
returned: always
type: bool
sample: false
ssh_enabled:
description: Whether the SSH subsystem is enabled.
returned: always
type: bool
sample: true
webdav_enabled:
description: Whether the WebDAV subsystem is enabled.
returned: always
type: bool
sample: false
readonly:
description: Whether the Subaccount is read-only.
returned: always
type: bool
sample: false
labels:
description: User-defined labels (key-value pairs) of the Storage Box Subaccount.
returned: always
type: dict
sample:
env: prod
created:
description: Point in time when the Storage Box Subaccount was created (in RFC3339 format).
returned: always
type: str
sample: "2025-12-03T13:47:47Z"
"""
import string
from ..module_utils import storage_box, storage_box_subaccount
from ..module_utils.client import client_resource_not_found
from ..module_utils.experimental import storage_box_experimental_warning
from ..module_utils.hcloud import AnsibleHCloud, AnsibleModule
from ..module_utils.storage_box_subaccount import NAME_LABEL_KEY
from ..module_utils.vendor.hcloud import HCloudException
from ..module_utils.vendor.hcloud.storage_boxes import (
BoundStorageBox,
BoundStorageBoxSubaccount,
StorageBoxSubaccountAccessSettings,
)
class AnsibleStorageBoxSubaccount(AnsibleHCloud):
represent = "storage_box_subaccount"
storage_box: BoundStorageBox | None = None
storage_box_subaccount: BoundStorageBoxSubaccount | None = None
storage_box_subaccount_name: str | None = None
def __init__(self, module: AnsibleModule):
storage_box_experimental_warning(module)
super().__init__(module)
def _prepare_result(self):
if self.storage_box_subaccount is None:
return {}
return storage_box_subaccount.prepare_result(self.storage_box_subaccount, self.storage_box_subaccount_name)
def _fetch(self):
self.storage_box = storage_box.get(self.client.storage_boxes, self.module.params.get("storage_box"))
if (value := self.module.params.get("id")) is not None:
self.storage_box_subaccount = self.storage_box.get_subaccount_by_id(value)
elif (value := self.module.params.get("name")) is not None:
self.storage_box_subaccount = storage_box_subaccount.get_by_name(self.storage_box, value)
# Workaround the missing name property
# Get the name of the resource from the labels
if self.storage_box_subaccount is not None:
self.storage_box_subaccount_name = self.storage_box_subaccount.labels.pop(NAME_LABEL_KEY)
def _create(self):
self.fail_on_invalid_params(
required=["name", "home_directory", "password"],
)
params = {
"home_directory": self.module.params.get("home_directory"),
"password": self.module.params.get("password"),
}
if (value := self.module.params.get("description")) is not None:
params["description"] = value
if (value := self.module.params.get("labels")) is not None:
params["labels"] = value
if (value := self.module.params.get("access_settings")) is not None:
params["access_settings"] = StorageBoxSubaccountAccessSettings.from_dict(value)
# Workaround the missing name property
# Save the name of the resource in the labels
if "labels" not in params:
params["labels"] = {}
params["labels"][NAME_LABEL_KEY] = self.module.params.get("name")
if not self.module.check_mode:
resp = self.storage_box.create_subaccount(**params)
self.storage_box_subaccount = resp.subaccount
resp.action.wait_until_finished()
self.storage_box_subaccount.reload()
self.storage_box_subaccount_name = self.storage_box_subaccount.labels.pop(NAME_LABEL_KEY)
self._mark_as_changed()
def _update(self):
need_reload = False
if (value := self.module.params.get("home_directory")) is not None:
if self.storage_box_subaccount.home_directory != value:
if not self.module.check_mode:
action = self.storage_box_subaccount.change_home_directory(value)
action.wait_until_finished()
need_reload = True
self._mark_as_changed()
if (value := self.module.params.get("access_settings")) is not None:
access_settings = StorageBoxSubaccountAccessSettings.from_dict(value)
if self.storage_box_subaccount.access_settings.to_payload() != access_settings.to_payload():
if not self.module.check_mode:
action = self.storage_box_subaccount.update_access_settings(access_settings)
action.wait_until_finished()
need_reload = True
self._mark_as_changed()
params = {}
if (value := self.module.params.get("description")) is not None:
if value != self.storage_box_subaccount.description:
params["description"] = value
self._mark_as_changed()
if (value := self.module.params.get("labels")) is not None:
if value != self.storage_box_subaccount.labels:
params["labels"] = value
self._mark_as_changed()
# Workaround the missing name property
# Preserve resource name in the labels, name update happens below
params["labels"][NAME_LABEL_KEY] = self.storage_box_subaccount_name
# Workaround the missing name property
# Update resource name in the labels
if (value := self.module.params.get("name")) is not None:
if value != self.storage_box_subaccount_name:
self.fail_on_invalid_params(required=["id"])
if "labels" not in params:
params["labels"] = self.storage_box_subaccount.labels
params["labels"][NAME_LABEL_KEY] = value
self._mark_as_changed()
# Update only if params holds changes or actions were triggered
if params or need_reload:
if not self.module.check_mode:
self.storage_box_subaccount = self.storage_box_subaccount.update(**params)
self.storage_box_subaccount_name = self.storage_box_subaccount.labels.pop(NAME_LABEL_KEY)
def _delete(self):
if not self.module.check_mode:
resp = self.storage_box_subaccount.delete()
resp.action.wait_until_finished()
self.storage_box_subaccount = None
self._mark_as_changed()
def present(self):
try:
self._fetch()
if self.storage_box_subaccount is None:
self._create()
else:
self._update()
except HCloudException as exception:
self.fail_json_hcloud(exception)
def absent(self):
try:
self._fetch()
if self.storage_box_subaccount is None:
return
self._delete()
except HCloudException as exception:
self.fail_json_hcloud(exception)
def reset_password(self):
self.fail_on_invalid_params(
required=["password"],
)
try:
self._fetch()
if self.storage_box_subaccount is None:
raise client_resource_not_found(
"storage box",
self.module.params.get("id") or self.module.params.get("name"),
)
if not self.module.check_mode:
action = self.storage_box_subaccount.reset_password(self.module.params.get("password"))
action.wait_until_finished()
self._mark_as_changed()
except HCloudException as exception:
self.fail_json_hcloud(exception)
@classmethod
def define_module(cls):
return AnsibleModule(
argument_spec=dict(
storage_box={"type": "str", "required": True},
id={"type": "int"},
name={"type": "str"},
home_directory={"type": "str"},
password={"type": "str", "no_log": True},
description={"type": "str"},
labels={"type": "dict"},
access_settings={
"type": "dict",
"options": dict(
reachable_externally={"type": "bool", "default": False},
samba_enabled={"type": "bool", "default": False},
ssh_enabled={"type": "bool", "default": False},
webdav_enabled={"type": "bool", "default": False},
readonly={"type": "bool", "default": False},
),
},
state={
"choices": ["absent", "present", "reset_password"],
"default": "present",
},
**super().base_module_arguments(),
),
required_one_of=[["id", "name"]],
supports_check_mode=True,
)
def main():
module = AnsibleStorageBoxSubaccount.define_module()
o = AnsibleStorageBoxSubaccount(module)
# Workaround the missing name property
# Validate name
if (value := module.params.get("name")) is not None:
if len(value) < 1:
module.fail_json(f"name '{value}' must be at least 1 character long")
allowed_chars = string.ascii_letters + string.digits + "-"
has_letters = False
for c in value:
if c in string.ascii_letters:
has_letters = True
if c in allowed_chars:
continue
module.fail_json(f"name '{value}' must only have allowed characters: {allowed_chars}")
if not has_letters:
module.fail_json(f"name '{value}' must contain at least one letter")
match module.params.get("state"):
case "reset_password":
o.reset_password()
case "absent":
o.absent()
case _:
o.present()
result = o.get_result()
# Legacy return value naming pattern
result["hcloud_storage_box_subaccount"] = result.pop(o.represent)
module.exit_json(**result)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,242 @@
#!/usr/bin/python
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
---
module: storage_box_subaccount_info
short_description: Gather infos about Hetzner Storage Box Subaccounts.
description:
- Gather infos about Hetzner Storage Box Subaccounts.
- See the L(Storage Box Subaccounts API documentation,https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts) for more details.
- B(Experimental:) Storage Box support is experimental, breaking changes may occur within minor releases.
See https://github.com/ansible-collections/hetzner.hcloud/issues/756 for more details.
author:
- Jonas Lammler (@jooola)
options:
storage_box:
description:
- ID or Name of the parent Storage Box.
- Using the ID is preferred, to reduce the amount of API requests.
type: str
required: true
id:
description:
- ID of the Storage Box Subaccount to get.
- If the ID is invalid, the module will fail.
type: int
name:
description:
- Name of the Storage Box Subaccount to get.
type: str
label_selector:
description:
- Label selector to filter the Storage Box Subaccounts to get.
type: str
extends_documentation_fragment:
- hetzner.hcloud.hcloud
"""
EXAMPLES = """
- name: Gather all Storage Box Subaccounts
hetzner.hcloud.storage_box_subaccount_info:
register: output
- name: Gather Storage Box Subaccounts by label
hetzner.hcloud.storage_box_subaccount_info:
label_selector: env=prod
register: output
- name: Gather a Storage Box Subaccount by name
hetzner.hcloud.storage_box_subaccount_info:
name: subaccount1
register: output
- name: Gather a Storage Box Subaccount by id
hetzner.hcloud.storage_box_subaccount_info:
name: 12345
register: output
- name: Print the gathered infos
debug:
var: output.hcloud_storage_box_subaccount_info
"""
RETURN = """
hcloud_storage_box_subaccount_info:
description: List of Storage Box Subaccounts.
returned: always
type: list
elements: dict
contains:
storage_box:
description: ID of the parent Storage Box.
returned: always
type: int
sample: 514605
id:
description: ID of the Storage Box Subaccount.
returned: always
type: int
sample: 158045
name:
description: Name of the Storage Box Subaccount.
returned: always
type: str
sample: subaccount1
description:
description: Description of the Storage Box Subaccount.
returned: always
type: str
sample: backups from subaccount1
home_directory:
description: Home directory of the Storage Box Subaccount.
returned: always
type: str
sample: backups/subaccount1
username:
description: Username of the Storage Box Subaccount.
returned: always
type: str
sample: u514605-sub1
server:
description: FQDN of the Storage Box Subaccount.
returned: always
type: str
sample: u514605-sub1.your-storagebox.de
access_settings:
description: Access settings of the Storage Box Subaccount.
returned: always
type: dict
contains:
reachable_externally:
description: Whether access from outside the Hetzner network is allowed.
returned: always
type: bool
sample: false
samba_enabled:
description: Whether the Samba subsystem is enabled.
returned: always
type: bool
sample: false
ssh_enabled:
description: Whether the SSH subsystem is enabled.
returned: always
type: bool
sample: true
webdav_enabled:
description: Whether the WebDAV subsystem is enabled.
returned: always
type: bool
sample: false
readonly:
description: Whether the Subaccount is read-only.
returned: always
type: bool
sample: false
labels:
description: User-defined labels (key-value pairs) of the Storage Box Subaccount.
returned: always
type: dict
sample:
env: prod
created:
description: Point in time when the Storage Box Subaccount was created (in RFC3339 format).
returned: always
type: str
sample: "2025-12-03T13:47:47Z"
"""
from ansible.module_utils.basic import AnsibleModule
from ..module_utils import storage_box, storage_box_subaccount
from ..module_utils.experimental import storage_box_experimental_warning
from ..module_utils.hcloud import AnsibleHCloud
from ..module_utils.storage_box_subaccount import NAME_LABEL_KEY
from ..module_utils.vendor.hcloud import HCloudException
from ..module_utils.vendor.hcloud.storage_boxes import (
BoundStorageBox,
BoundStorageBoxSubaccount,
)
class AnsibleStorageBoxSubaccountInfo(AnsibleHCloud):
represent = "storage_box_subaccounts"
storage_box: BoundStorageBox | None = None
storage_box_subaccounts: list[BoundStorageBoxSubaccount] | None = None
def __init__(self, module: AnsibleModule):
storage_box_experimental_warning(module)
super().__init__(module)
def _prepare_result(self):
result = []
for o in self.storage_box_subaccounts or []:
if o is not None:
# Workaround the missing name property
# Get the name of the resource from the labels
name = o.labels.pop(NAME_LABEL_KEY)
result.append(storage_box_subaccount.prepare_result(o, name))
return result
def fetch(self):
try:
self.storage_box = storage_box.get(
self.client.storage_boxes,
self.module.params.get("storage_box"),
)
if (value := self.module.params.get("id")) is not None:
self.storage_box_subaccounts = [self.storage_box.get_subaccount_by_id(value)]
elif (value := self.module.params.get("name")) is not None:
self.storage_box_subaccounts = [storage_box_subaccount.get_by_name(self.storage_box, value)]
else:
params = {}
if (value := self.module.params.get("label_selector")) is not None:
params["label_selector"] = value
self.storage_box_subaccounts = self.storage_box.get_subaccount_all(**params)
except HCloudException as exception:
self.fail_json_hcloud(exception)
@classmethod
def define_module(cls):
return AnsibleModule(
argument_spec=dict(
storage_box={"type": "str", "required": True},
id={"type": "int"},
name={"type": "str"},
label_selector={"type": "str"},
**super().base_module_arguments(),
),
supports_check_mode=True,
)
def main():
module = AnsibleStorageBoxSubaccountInfo.define_module()
o = AnsibleStorageBoxSubaccountInfo(module)
o.fetch()
result = o.get_result()
# Legacy return value naming pattern
result["hcloud_storage_box_subaccount_info"] = result.pop(o.represent)
module.exit_json(**result)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,194 @@
#!/usr/bin/python
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
---
module: storage_box_type_info
short_description: Gather infos about Hetzner Storage Box Types.
description:
- Gather infos about available Hetzner Storage Box Types.
- See the L(Storage Box Types documentation,https://docs.hetzner.cloud/reference/hetzner#storage-box-types) for more details.
- B(Experimental:) Storage Box support is experimental, breaking changes may occur within minor releases.
See https://github.com/ansible-collections/hetzner.hcloud/issues/756 for more details.
author:
- Jonas Lammler (@jooola)
options:
id:
description:
- ID of the Storage Box Type to get.
- If the ID is invalid, the module will fail.
type: int
name:
description:
- Name of the Storage Box Type to get.
type: str
extends_documentation_fragment:
- hetzner.hcloud.hcloud
"""
EXAMPLES = """
- name: Gather Storage Box Types infos
hetzner.hcloud.storage_box_type_info:
register: output
- name: Print the gathered infos
debug:
var: output.hcloud_storage_box_type_info
"""
RETURN = """
hcloud_storage_box_type_info:
description: List of Storage Box Types.
returned: always
type: list
contains:
id:
description: ID of the Storage Box Type.
returned: success
type: int
sample: 1937415
name:
description: Name of the Storage Box Type.
returned: success
type: str
sample: bx21
description:
description: Description of the Storage Box Type.
returned: success
type: str
sample: BX21
snapshot_limit:
description: Maximum number of allowed manual snapshots.
returned: success
type: int
sample: 10
automatic_snapshot_limit:
description: Maximum number of snapshots created automatically by a snapshot plan.
returned: success
type: int
sample: 10
subaccounts_limit:
description: Maximum number of subaccounts.
returned: success
type: int
sample: 200
size:
description: Available storage in bytes.
returned: success
type: int
sample: 1099511627776
deprecation:
description: Deprecation details about the Storage Box Type.
returned: when deprecated
type: dict
contains:
announced:
description: Date when the deprecation was announced.
returned: success
type: str
sample: "2025-06-02T00:00:00Z"
unavailable_after:
description: |
Date when the resource will stop being available.
The resource will be removed from the list endpoint, but details
about the resource can be fetched using its ID.
returned: success
type: str
sample: "2025-09-02T00:00:00Z"
"""
from ansible.module_utils.basic import AnsibleModule
from ..module_utils.experimental import storage_box_experimental_warning
from ..module_utils.hcloud import AnsibleHCloud
from ..module_utils.vendor.hcloud import HCloudException
from ..module_utils.vendor.hcloud.storage_box_types import BoundStorageBoxType
class AnsibleStorageBoxTypeInfo(AnsibleHCloud):
represent = "storage_box_types"
storage_box_types: list[BoundStorageBoxType] | None = None
def __init__(self, module: AnsibleModule):
storage_box_experimental_warning(module)
super().__init__(module)
def _prepare_result(self):
result = []
for o in self.storage_box_types or []:
if o is None:
continue
result.append(
{
"id": o.id,
"name": o.name,
"description": o.description,
"snapshot_limit": o.snapshot_limit,
"automatic_snapshot_limit": o.automatic_snapshot_limit,
"subaccounts_limit": o.subaccounts_limit,
"size": o.size,
"deprecation": (
{
"announced": o.deprecation.announced.isoformat(),
"unavailable_after": o.deprecation.unavailable_after.isoformat(),
}
if o.deprecation is not None
else None
),
}
)
return result
def fetch(self):
try:
if (id_ := self.module.params.get("id")) is not None:
self.storage_box_types = [self.client.storage_box_types.get_by_id(id_)]
elif (name := self.module.params.get("name")) is not None:
self.storage_box_types = [self.client.storage_box_types.get_by_name(name)]
else:
self.storage_box_types = self.client.storage_box_types.get_all()
except HCloudException as exception:
self.fail_json_hcloud(exception)
@classmethod
def define_module(cls):
return AnsibleModule(
argument_spec=dict(
id={"type": "int"},
name={"type": "str"},
**super().base_module_arguments(),
),
supports_check_mode=True,
)
def main():
module = AnsibleStorageBoxTypeInfo.define_module()
o = AnsibleStorageBoxTypeInfo(module)
o.fetch()
result = o.get_result()
# Legacy return value naming pattern
result["hcloud_storage_box_type_info"] = result.pop(o.represent)
module.exit_json(**result)
if __name__ == "__main__":
main()

View file

@ -24,3 +24,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -27,3 +27,9 @@ hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -0,0 +1,3 @@
cloud/hcloud
gather_facts/no
azp/group2

View file

@ -0,0 +1,35 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
# Azure Pipelines will configure this value to something similar to
# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i"
hcloud_prefix: "tests"
# Used to namespace resources created by concurrent test pipelines/targets
hcloud_run_ns: "{{ hcloud_prefix | md5 }}"
hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}"
hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}"
# Used to easily update the server types and images across all our tests.
hcloud_server_type_name: cax11
hcloud_server_type_id: 45
hcloud_server_type_upgrade_name: cax21
hcloud_server_type_upgrade_id: 93
hcloud_image_name: debian-12
hcloud_image_id: 114690389 # architecture=arm
hcloud_location_name: hel1
hcloud_location_id: 3
hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -0,0 +1,6 @@
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
hcloud_storage_box_name: "{{ hcloud_ns }}"
hcloud_ssh_key_name: "{{ hcloud_ns }}"
hcloud_storage_box_password: 1-secret-PASSW0RD-=)

View file

@ -0,0 +1,3 @@
---
dependencies:
- setup_ssh_keypair

View file

@ -0,0 +1,5 @@
---
- name: Cleanup test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
state: absent

View file

@ -0,0 +1,31 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
- name: Check if cleanup.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/cleanup.yml"
register: cleanup_file
- name: Check if prepare.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/prepare.yml"
register: prepare_file
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists
- name: Include prepare tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml"
when: prepare_file.stat.exists
- name: Run tests
block:
- name: Include test tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml"
always:
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists

View file

@ -0,0 +1 @@
---

View file

@ -0,0 +1,247 @@
---
- name: Test missing required parameters
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
state: present
ignore_errors: true
register: result
- name: Verify missing required parameters
ansible.builtin.assert:
that:
- result is failed
- 'result.msg == "missing required arguments: storage_box_type, location, password"'
- name: Test create with check mode
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
type: "{{ hcloud_storage_box_type_name }}"
location: "{{ hcloud_location_name }}"
password: "{{ hcloud_storage_box_password }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
labels:
key: value
check_mode: true
register: result
- name: Verify create with check mode
ansible.builtin.assert:
that:
- result is changed
- name: Test create
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
type: "{{ hcloud_storage_box_type_name }}"
location: "{{ hcloud_location_name }}"
password: "{{ hcloud_storage_box_password }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
access_settings:
ssh_enabled: true
zfs_enabled: false
snapshot_plan:
max_snapshots: 10
hour: 3
minute: 30
labels:
key: value
register: result
- name: Verify create
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box.id is not none
- result.hcloud_storage_box.name == hcloud_storage_box_name
- result.hcloud_storage_box.storage_box_type == hcloud_storage_box_type_name
- result.hcloud_storage_box.location == hcloud_location_name
- result.hcloud_storage_box.labels.key == "value"
- result.hcloud_storage_box.delete_protection == false
- result.hcloud_storage_box.access_settings.reachable_externally == false
- result.hcloud_storage_box.access_settings.samba_enabled == false
- result.hcloud_storage_box.access_settings.ssh_enabled == true
- result.hcloud_storage_box.access_settings.webdav_enabled == false
- result.hcloud_storage_box.access_settings.zfs_enabled == false
- result.hcloud_storage_box.snapshot_plan.max_snapshots == 10
- result.hcloud_storage_box.snapshot_plan.hour == 3
- result.hcloud_storage_box.snapshot_plan.minute == 30
- result.hcloud_storage_box.status == "active"
- result.hcloud_storage_box.server is not none
- result.hcloud_storage_box.system is not none
- result.hcloud_storage_box.username is not none
- result.hcloud_storage_box.stats.size >= 0
- result.hcloud_storage_box.stats.size_data >= 0
- result.hcloud_storage_box.stats.size_snapshots >= 0
- name: Test create idempotency
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
type: "{{ hcloud_storage_box_type_name }}"
location: "{{ hcloud_location_name }}"
password: "{{ hcloud_storage_box_password }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
access_settings:
ssh_enabled: true
zfs_enabled: false
snapshot_plan:
max_snapshots: 10
hour: 3
minute: 30
labels:
key: value
register: result
- name: Verify create idempotency
ansible.builtin.assert:
that:
- result is not changed
- name: Test update name
hetzner.hcloud.storage_box:
id: "{{ result.hcloud_storage_box.id }}"
name: "{{ hcloud_storage_box_name }}-changed" # Update
register: result
- name: Verify update name
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box.name == hcloud_storage_box_name + "-changed"
# Ensure snapshot plan was not changed
- result.hcloud_storage_box.snapshot_plan.max_snapshots == 10
- result.hcloud_storage_box.snapshot_plan.hour == 3
- result.hcloud_storage_box.snapshot_plan.minute == 30
- name: Test update
hetzner.hcloud.storage_box:
id: "{{ result.hcloud_storage_box.id }}"
name: "{{ hcloud_storage_box_name }}" # Update
type: "{{ hcloud_storage_box_type_upgrade_name }}" # Update
location: "{{ hcloud_location_name }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
access_settings: # Update
ssh_enabled: false
reachable_externally: true
snapshot_plan:
max_snapshots: 10
hour: 4 # Update
minute: 30
labels:
key: changed # Update
delete_protection: true # Update
register: result
- name: Verify update
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box.id is not none
- result.hcloud_storage_box.name == hcloud_storage_box_name
- result.hcloud_storage_box.storage_box_type == hcloud_storage_box_type_upgrade_name
- result.hcloud_storage_box.location == hcloud_location_name
- result.hcloud_storage_box.labels.key == "changed"
- result.hcloud_storage_box.delete_protection == true
- result.hcloud_storage_box.access_settings.reachable_externally == true
- result.hcloud_storage_box.access_settings.samba_enabled == false
- result.hcloud_storage_box.access_settings.ssh_enabled == false
- result.hcloud_storage_box.access_settings.webdav_enabled == false
- result.hcloud_storage_box.access_settings.zfs_enabled == false
- result.hcloud_storage_box.snapshot_plan.max_snapshots == 10
- result.hcloud_storage_box.snapshot_plan.hour == 4
- result.hcloud_storage_box.snapshot_plan.minute == 30
- name: Test update idempotency
hetzner.hcloud.storage_box:
id: "{{ result.hcloud_storage_box.id }}"
name: "{{ hcloud_storage_box_name }}" # Update
type: "{{ hcloud_storage_box_type_upgrade_name }}" # Update
location: "{{ hcloud_location_name }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
access_settings: # Update
ssh_enabled: false
reachable_externally: true
snapshot_plan:
max_snapshots: 10
hour: 4 # Update
minute: 30
labels:
key: changed # Update
delete_protection: true # Update
register: result
- name: Verify update idempotency
ansible.builtin.assert:
that:
- result is not changed
- name: Test delete with delete protection
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
state: absent
ignore_errors: true
register: result
- name: Verify delete with delete protection
ansible.builtin.assert:
that:
- result is failed
- result.failure.code == "protected"
- name: Test update delete protection
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
delete_protection: false
register: result
- name: Verify update delete protection
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box.delete_protection == false
- name: Test update snapshot plan
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
snapshot_plan: null
register: result
- name: Verify update snapshot plan
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box.snapshot_plan is none
- name: Test reset password
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
password: "{{ hcloud_storage_box_password }}"
state: reset_password
register: result
- name: Verify reset password
ansible.builtin.assert:
that:
- result is changed
- name: Create snapshot
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ hcloud_storage_box_name }}"
description: test-rollback
register: _rollback_snapshot
- name: Test rollback snapshot
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
snapshot: "{{ _rollback_snapshot.hcloud_storage_box_snapshot.id }}"
state: rollback_snapshot
register: result
- name: Verify rollback snapshot
ansible.builtin.assert:
that:
- result is changed
- name: Test delete
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
state: absent
register: result
- name: Verify delete
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box is none

View file

@ -0,0 +1,3 @@
cloud/hcloud
gather_facts/no
azp/group2

View file

@ -0,0 +1,35 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
# Azure Pipelines will configure this value to something similar to
# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i"
hcloud_prefix: "tests"
# Used to namespace resources created by concurrent test pipelines/targets
hcloud_run_ns: "{{ hcloud_prefix | md5 }}"
hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}"
hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}"
# Used to easily update the server types and images across all our tests.
hcloud_server_type_name: cax11
hcloud_server_type_id: 45
hcloud_server_type_upgrade_name: cax21
hcloud_server_type_upgrade_id: 93
hcloud_image_name: debian-12
hcloud_image_id: 114690389 # architecture=arm
hcloud_location_name: hel1
hcloud_location_id: 3
hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -0,0 +1,6 @@
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
hcloud_storage_box_name: "{{ hcloud_ns }}"
hcloud_ssh_key_name: "{{ hcloud_ns }}"
hcloud_storage_box_password: 1-secret-PASSW0RD-=)

View file

@ -0,0 +1,3 @@
---
dependencies:
- setup_ssh_keypair

View file

@ -0,0 +1,5 @@
---
- name: Cleanup test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
state: absent

View file

@ -0,0 +1,31 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
- name: Check if cleanup.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/cleanup.yml"
register: cleanup_file
- name: Check if prepare.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/prepare.yml"
register: prepare_file
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists
- name: Include prepare tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml"
when: prepare_file.stat.exists
- name: Run tests
block:
- name: Include test tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml"
always:
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists

View file

@ -0,0 +1,15 @@
---
- name: Create test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
type: "{{ hcloud_storage_box_type_name }}"
location: "{{ hcloud_location_name }}"
password: "{{ hcloud_storage_box_password }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
access_settings:
ssh_enabled: true
zfs_enabled: false
labels:
key: "{{ hcloud_ns }}"
register: test_storage_box

View file

@ -0,0 +1,83 @@
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Gather hcloud_storage_box_info
hetzner.hcloud.storage_box_info:
register: result
- name: Verify hcloud_storage_box_info
ansible.builtin.assert:
that:
- result.hcloud_storage_box_info | list | count >= 1
- name: Gather hcloud_storage_box_info in check mode
hetzner.hcloud.storage_box_info:
check_mode: true
register: result
- name: Verify hcloud_storage_box_info in check mode
ansible.builtin.assert:
that:
- result.hcloud_storage_box_info | list | count >= 1
- name: Gather hcloud_storage_box_info with label
hetzner.hcloud.storage_box_info:
label_selector: key={{ hcloud_ns }}
register: result
- name: Verify hcloud_storage_box_info
ansible.builtin.assert:
that:
- result.hcloud_storage_box_info | list | count == 1
- name: Gather hcloud_storage_box_info with correct id
hetzner.hcloud.storage_box_info:
id: "{{ test_storage_box.hcloud_storage_box.id }}"
register: result
- name: Verify hcloud_storage_box_info with correct id
ansible.builtin.assert:
that:
- result.hcloud_storage_box_info | list | count == 1
- result.hcloud_storage_box_info[0].id == test_storage_box.hcloud_storage_box.id
- result.hcloud_storage_box_info[0].name == hcloud_storage_box_name
- result.hcloud_storage_box_info[0].storage_box_type == hcloud_storage_box_type_name
- result.hcloud_storage_box_info[0].location == hcloud_location_name
- result.hcloud_storage_box_info[0].labels.key == hcloud_ns
- result.hcloud_storage_box_info[0].delete_protection == false
- result.hcloud_storage_box_info[0].access_settings.reachable_externally == false
- result.hcloud_storage_box_info[0].access_settings.samba_enabled == false
- result.hcloud_storage_box_info[0].access_settings.ssh_enabled == true
- result.hcloud_storage_box_info[0].access_settings.webdav_enabled == false
- result.hcloud_storage_box_info[0].access_settings.zfs_enabled == false
- result.hcloud_storage_box_info[0].status == "active"
- result.hcloud_storage_box_info[0].server is not none
- result.hcloud_storage_box_info[0].system is not none
- result.hcloud_storage_box_info[0].username is not none
- result.hcloud_storage_box_info[0].stats.size >= 0
- result.hcloud_storage_box_info[0].stats.size_data >= 0
- result.hcloud_storage_box_info[0].stats.size_snapshots >= 0
- name: Gather hcloud_storage_box_info with wrong id
hetzner.hcloud.storage_box_info:
id: "{{ test_storage_box.hcloud_storage_box.id }}4321"
ignore_errors: true
register: result
- name: Verify hcloud_storage_box_info with wrong id
ansible.builtin.assert:
that:
- result is failed
- name: Gather hcloud_storage_box_info with correct name
hetzner.hcloud.storage_box_info:
name: "{{ hcloud_storage_box_name }}"
register: result
- name: Verify hcloud_storage_box_info with correct name
ansible.builtin.assert:
that:
- result.hcloud_storage_box_info | list | count == 1
- name: Gather hcloud_storage_box_info with wrong name
hetzner.hcloud.storage_box_info:
name: "{{ hcloud_storage_box_name }}-invalid"
register: result
- name: Verify hcloud_storage_box_info with wrong name
ansible.builtin.assert:
that:
- result.hcloud_storage_box_info | list | count == 0

View file

@ -0,0 +1,3 @@
cloud/hcloud
gather_facts/no
azp/group2

View file

@ -0,0 +1,35 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
# Azure Pipelines will configure this value to something similar to
# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i"
hcloud_prefix: "tests"
# Used to namespace resources created by concurrent test pipelines/targets
hcloud_run_ns: "{{ hcloud_prefix | md5 }}"
hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}"
hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}"
# Used to easily update the server types and images across all our tests.
hcloud_server_type_name: cax11
hcloud_server_type_id: 45
hcloud_server_type_upgrade_name: cax21
hcloud_server_type_upgrade_id: 93
hcloud_image_name: debian-12
hcloud_image_id: 114690389 # architecture=arm
hcloud_location_name: hel1
hcloud_location_id: 3
hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -0,0 +1,6 @@
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
hcloud_storage_box_name: "{{ hcloud_ns }}"
hcloud_ssh_key_name: "{{ hcloud_ns }}"
hcloud_storage_box_password: 1-secret-PASSW0RD-=)

View file

@ -0,0 +1,3 @@
---
dependencies:
- setup_ssh_keypair

View file

@ -0,0 +1,5 @@
---
- name: Cleanup test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
state: absent

View file

@ -0,0 +1,31 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
- name: Check if cleanup.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/cleanup.yml"
register: cleanup_file
- name: Check if prepare.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/prepare.yml"
register: prepare_file
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists
- name: Include prepare tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml"
when: prepare_file.stat.exists
- name: Run tests
block:
- name: Include test tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml"
always:
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists

View file

@ -0,0 +1,15 @@
---
- name: Create test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
type: "{{ hcloud_storage_box_type_name }}"
location: "{{ hcloud_location_name }}"
password: "{{ hcloud_storage_box_password }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
access_settings:
ssh_enabled: true
reachable_externally: false
labels:
key: "{{ hcloud_ns }}"
register: test_storage_box

View file

@ -0,0 +1,119 @@
---
- name: Test missing required parameters # noqa: args[module]
hetzner.hcloud.storage_box_snapshot:
ignore_errors: true
register: result
- name: Verify missing required parameters
ansible.builtin.assert:
that:
- result is failed
- 'result.msg == "missing required arguments: storage_box"'
- name: Test create with check mode
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ hcloud_storage_box_name }}"
description: migration-1
labels:
key: value
check_mode: true
register: result
- name: Verify create with check mode
ansible.builtin.assert:
that:
- result is changed
- name: Test create
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ hcloud_storage_box_name }}"
description: migration-1
labels:
key: value
register: result
- name: Verify create
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box_snapshot.storage_box == test_storage_box.hcloud_storage_box.id
- result.hcloud_storage_box_snapshot.id is not none
- result.hcloud_storage_box_snapshot.name is not none
- result.hcloud_storage_box_snapshot.description == "migration-1"
- result.hcloud_storage_box_snapshot.labels.key == "value"
- result.hcloud_storage_box_snapshot.is_automatic is false
- result.hcloud_storage_box_snapshot.stats.size >= 0
- result.hcloud_storage_box_snapshot.stats.size_filesystem >= 0
- result.hcloud_storage_box_snapshot.created is not none
- name: Save Storage Box Snapshot
ansible.builtin.set_fact:
_storage_box_snapshot: "{{ result.hcloud_storage_box_snapshot }}"
- name: Test create idempotency
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
id: "{{ _storage_box_snapshot.id }}" # Create is not idempotent, only with id or name
description: migration-1
labels:
key: value
register: result
- name: Verify create idempotency
ansible.builtin.assert:
that:
- result is not changed
- name: Test update
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
id: "{{ _storage_box_snapshot.id }}"
description: migration-changed # Update
labels:
key: changed # Update
register: result
- name: Verify update
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box_snapshot.storage_box == test_storage_box.hcloud_storage_box.id
- result.hcloud_storage_box_snapshot.id is not none
- result.hcloud_storage_box_snapshot.name is not none
- result.hcloud_storage_box_snapshot.description == "migration-changed"
- result.hcloud_storage_box_snapshot.labels.key == "changed"
- result.hcloud_storage_box_snapshot.is_automatic is false
- result.hcloud_storage_box_snapshot.stats.size >= 0
- result.hcloud_storage_box_snapshot.stats.size_filesystem >= 0
- result.hcloud_storage_box_snapshot.created is not none
- name: Test update idempotency
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ test_storage_box.hcloud_storage_box.name }}"
name: "{{ _storage_box_snapshot.name }}"
description: migration-changed # Update
labels:
key: changed # Update
register: result
- name: Verify update idempotency
ansible.builtin.assert:
that:
- result is not changed
- name: Test delete
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ test_storage_box.hcloud_storage_box.name }}"
name: "{{ _storage_box_snapshot.name }}"
state: absent
register: result
- name: Verify delete
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box_snapshot is none
- name: Test delete idempotency
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ test_storage_box.hcloud_storage_box.name }}"
name: "{{ _storage_box_snapshot.name }}"
state: absent
register: result
- name: Verify delete idempotency
ansible.builtin.assert:
that:
- result is not changed

View file

@ -0,0 +1,3 @@
cloud/hcloud
gather_facts/no
azp/group2

View file

@ -0,0 +1,35 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
# Azure Pipelines will configure this value to something similar to
# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i"
hcloud_prefix: "tests"
# Used to namespace resources created by concurrent test pipelines/targets
hcloud_run_ns: "{{ hcloud_prefix | md5 }}"
hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}"
hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}"
# Used to easily update the server types and images across all our tests.
hcloud_server_type_name: cax11
hcloud_server_type_id: 45
hcloud_server_type_upgrade_name: cax21
hcloud_server_type_upgrade_id: 93
hcloud_image_name: debian-12
hcloud_image_id: 114690389 # architecture=arm
hcloud_location_name: hel1
hcloud_location_id: 3
hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -0,0 +1,6 @@
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
hcloud_storage_box_name: "{{ hcloud_ns }}"
hcloud_ssh_key_name: "{{ hcloud_ns }}"
hcloud_storage_box_password: 1-secret-PASSW0RD-=)

View file

@ -0,0 +1,3 @@
---
dependencies:
- setup_ssh_keypair

View file

@ -0,0 +1,5 @@
---
- name: Cleanup test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
state: absent

View file

@ -0,0 +1,31 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
- name: Check if cleanup.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/cleanup.yml"
register: cleanup_file
- name: Check if prepare.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/prepare.yml"
register: prepare_file
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists
- name: Include prepare tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml"
when: prepare_file.stat.exists
- name: Run tests
block:
- name: Include test tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml"
always:
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists

View file

@ -0,0 +1,28 @@
---
- name: Create test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
type: "{{ hcloud_storage_box_type_name }}"
location: "{{ hcloud_location_name }}"
password: "{{ hcloud_storage_box_password }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
access_settings:
ssh_enabled: true
reachable_externally: false
labels:
key: "{{ hcloud_ns }}"
register: test_storage_box
- name: Create test_storage_box_snapshot1
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
description: snapshot1
labels:
key: "{{ hcloud_ns }}"
register: test_storage_box_snapshot1
- name: Create test_storage_box_snapshot2
hetzner.hcloud.storage_box_snapshot:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
register: test_storage_box_snapshot2

View file

@ -0,0 +1,81 @@
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Gather hcloud_storage_box_snapshot_info
hetzner.hcloud.storage_box_snapshot_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
register: result
- name: Verify hcloud_storage_box_snapshot_info
ansible.builtin.assert:
that:
- result.hcloud_storage_box_snapshot_info | list | count == 2
- name: Gather hcloud_storage_box_snapshot_info in check mode
hetzner.hcloud.storage_box_snapshot_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
check_mode: true
register: result
- name: Verify hcloud_storage_box_snapshot_info in check mode
ansible.builtin.assert:
that:
- result.hcloud_storage_box_snapshot_info | list | count == 2
- name: Gather hcloud_storage_box_snapshot_info with label
hetzner.hcloud.storage_box_snapshot_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
label_selector: "key={{ hcloud_ns }}"
register: result
- name: Verify hcloud_storage_box_snapshot_info
ansible.builtin.assert:
that:
- result.hcloud_storage_box_snapshot_info | list | count == 1
- name: Gather hcloud_storage_box_snapshot_info with correct id
hetzner.hcloud.storage_box_snapshot_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
id: "{{ test_storage_box_snapshot1.hcloud_storage_box_snapshot.id }}"
register: result
- name: Verify hcloud_storage_box_snapshot_info with correct id
ansible.builtin.assert:
that:
- result.hcloud_storage_box_snapshot_info | list | count == 1
- result.hcloud_storage_box_snapshot_info[0].storage_box == test_storage_box.hcloud_storage_box.id
- result.hcloud_storage_box_snapshot_info[0].id is not none
- result.hcloud_storage_box_snapshot_info[0].name is not none
- result.hcloud_storage_box_snapshot_info[0].description == "snapshot1"
- result.hcloud_storage_box_snapshot_info[0].labels.key == hcloud_ns
- result.hcloud_storage_box_snapshot_info[0].is_automatic is false
- result.hcloud_storage_box_snapshot_info[0].stats.size >= 0
- result.hcloud_storage_box_snapshot_info[0].stats.size_filesystem >= 0
- result.hcloud_storage_box_snapshot_info[0].created is not none
- name: Gather hcloud_storage_box_snapshot_info with wrong id
hetzner.hcloud.storage_box_snapshot_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
id: "{{ test_storage_box_snapshot1.hcloud_storage_box_snapshot.id }}4321"
ignore_errors: true
register: result
- name: Verify hcloud_storage_box_snapshot_info with wrong id
ansible.builtin.assert:
that:
- result is failed
- name: Gather hcloud_storage_box_snapshot_info with correct name
hetzner.hcloud.storage_box_snapshot_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: "{{ test_storage_box_snapshot1.hcloud_storage_box_snapshot.name }}"
register: result
- name: Verify hcloud_storage_box_snapshot_info with correct name
ansible.builtin.assert:
that:
- result.hcloud_storage_box_snapshot_info | list | count == 1
- name: Gather hcloud_storage_box_snapshot_info with wrong name
hetzner.hcloud.storage_box_snapshot_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: "{{ test_storage_box_snapshot1.hcloud_storage_box_snapshot.name }}-invalid"
register: result
- name: Verify hcloud_storage_box_snapshot_info with wrong name
ansible.builtin.assert:
that:
- result.hcloud_storage_box_snapshot_info | list | count == 0

View file

@ -0,0 +1,3 @@
cloud/hcloud
gather_facts/no
azp/group2

View file

@ -0,0 +1,35 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
# Azure Pipelines will configure this value to something similar to
# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i"
hcloud_prefix: "tests"
# Used to namespace resources created by concurrent test pipelines/targets
hcloud_run_ns: "{{ hcloud_prefix | md5 }}"
hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}"
hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}"
# Used to easily update the server types and images across all our tests.
hcloud_server_type_name: cax11
hcloud_server_type_id: 45
hcloud_server_type_upgrade_name: cax21
hcloud_server_type_upgrade_id: 93
hcloud_image_name: debian-12
hcloud_image_id: 114690389 # architecture=arm
hcloud_location_name: hel1
hcloud_location_id: 3
hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -0,0 +1,6 @@
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
hcloud_storage_box_name: "{{ hcloud_ns }}"
hcloud_ssh_key_name: "{{ hcloud_ns }}"
hcloud_storage_box_password: 1-secret-PASSW0RD-=)

View file

@ -0,0 +1,3 @@
---
dependencies:
- setup_ssh_keypair

View file

@ -0,0 +1,5 @@
---
- name: Cleanup test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
state: absent

View file

@ -0,0 +1,31 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
- name: Check if cleanup.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/cleanup.yml"
register: cleanup_file
- name: Check if prepare.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/prepare.yml"
register: prepare_file
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists
- name: Include prepare tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml"
when: prepare_file.stat.exists
- name: Run tests
block:
- name: Include test tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml"
always:
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists

View file

@ -0,0 +1,15 @@
---
- name: Create test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
type: "{{ hcloud_storage_box_type_name }}"
location: "{{ hcloud_location_name }}"
password: "{{ hcloud_storage_box_password }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
access_settings:
ssh_enabled: true
reachable_externally: false
labels:
key: "{{ hcloud_ns }}"
register: test_storage_box

View file

@ -0,0 +1,190 @@
---
- name: Test missing required parameters # noqa: args[module]
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ hcloud_storage_box_name }}"
ignore_errors: true
register: result
- name: Verify missing required parameters
ansible.builtin.assert:
that:
- result is failed
- 'result.msg == "one of the following is required: id, name"'
- name: Test create with check mode
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ hcloud_storage_box_name }}"
name: subaccount1
description: backups from subaccount1
home_directory: backups/subaccount1
password: "{{ hcloud_storage_box_password }}"
labels:
key: value
access_settings:
ssh_enabled: true
readonly: false
check_mode: true
register: result
- name: Verify create with check mode
ansible.builtin.assert:
that:
- result is changed
- name: Test create
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ hcloud_storage_box_name }}"
name: subaccount1
description: backups from subaccount1
home_directory: backups/subaccount1
password: "{{ hcloud_storage_box_password }}"
labels:
key: value
access_settings:
ssh_enabled: true
readonly: false
register: result
- name: Verify create
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box_subaccount.storage_box == test_storage_box.hcloud_storage_box.id
- result.hcloud_storage_box_subaccount.id is not none
- result.hcloud_storage_box_subaccount.name == "subaccount1"
- result.hcloud_storage_box_subaccount.description == "backups from subaccount1"
- result.hcloud_storage_box_subaccount.home_directory == "backups/subaccount1"
- result.hcloud_storage_box_subaccount.username is not none
- result.hcloud_storage_box_subaccount.server is not none
- result.hcloud_storage_box_subaccount.labels.key == "value"
- result.hcloud_storage_box_subaccount.access_settings.reachable_externally == false
- result.hcloud_storage_box_subaccount.access_settings.samba_enabled == false
- result.hcloud_storage_box_subaccount.access_settings.ssh_enabled == true
- result.hcloud_storage_box_subaccount.access_settings.webdav_enabled == false
- result.hcloud_storage_box_subaccount.access_settings.readonly == false
- result.hcloud_storage_box_subaccount.created is not none
- name: Test create idempotency
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: subaccount1
description: backups from subaccount1
home_directory: backups/subaccount1
password: "{{ hcloud_storage_box_password }}"
labels:
key: value
access_settings:
ssh_enabled: true
readonly: false
register: result
- name: Verify create idempotency
ansible.builtin.assert:
that:
- result is not changed
- name: Test update
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: subaccount1
description: backups from subaccount2 # Update
home_directory: backups/subaccount2 # Update
password: "{{ hcloud_storage_box_password }}"
labels:
key: changed # Update
access_settings:
ssh_enabled: true
readonly: true # Update
register: result
- name: Verify update
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box_subaccount.storage_box == test_storage_box.hcloud_storage_box.id
- result.hcloud_storage_box_subaccount.id is not none
- result.hcloud_storage_box_subaccount.name == "subaccount1"
- result.hcloud_storage_box_subaccount.description == "backups from subaccount2"
- result.hcloud_storage_box_subaccount.home_directory == "backups/subaccount2"
- result.hcloud_storage_box_subaccount.username is not none
- result.hcloud_storage_box_subaccount.server is not none
- result.hcloud_storage_box_subaccount.labels.key == "changed"
- result.hcloud_storage_box_subaccount.access_settings.reachable_externally == false
- result.hcloud_storage_box_subaccount.access_settings.samba_enabled == false
- result.hcloud_storage_box_subaccount.access_settings.ssh_enabled == true
- result.hcloud_storage_box_subaccount.access_settings.webdav_enabled == false
- result.hcloud_storage_box_subaccount.access_settings.readonly == true
- result.hcloud_storage_box_subaccount.created is not none
- name: Test update idempotency
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: subaccount1
description: backups from subaccount2 # Update
home_directory: backups/subaccount2 # Update
password: "{{ hcloud_storage_box_password }}"
labels:
key: changed # Update
access_settings:
ssh_enabled: true
readonly: true # Update
register: result
- name: Verify update idempotency
ansible.builtin.assert:
that:
- result is not changed
- name: Test update name
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
id: "{{ result.hcloud_storage_box_subaccount.id }}"
name: subaccount2
register: result
- name: Verify update
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box_subaccount.storage_box == test_storage_box.hcloud_storage_box.id
- result.hcloud_storage_box_subaccount.id is not none
- result.hcloud_storage_box_subaccount.name == "subaccount2" # Update
- result.hcloud_storage_box_subaccount.description == "backups from subaccount2"
- result.hcloud_storage_box_subaccount.home_directory == "backups/subaccount2"
- result.hcloud_storage_box_subaccount.username is not none
- result.hcloud_storage_box_subaccount.server is not none
- result.hcloud_storage_box_subaccount.labels.key == "changed"
- result.hcloud_storage_box_subaccount.access_settings.reachable_externally == false
- result.hcloud_storage_box_subaccount.access_settings.samba_enabled == false
- result.hcloud_storage_box_subaccount.access_settings.ssh_enabled == true
- result.hcloud_storage_box_subaccount.access_settings.webdav_enabled == false
- result.hcloud_storage_box_subaccount.access_settings.readonly == true
- result.hcloud_storage_box_subaccount.created is not none
- name: Test reset password
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: subaccount2
password: "{{ hcloud_storage_box_password }}"
state: reset_password
register: result
- name: Verify reset password
ansible.builtin.assert:
that:
- result is changed
- name: Test delete
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: subaccount2
state: absent
register: result
- name: Verify delete
ansible.builtin.assert:
that:
- result is changed
- result.hcloud_storage_box_subaccount is none
- name: Test delete idempotency
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: subaccount2
state: absent
register: result
- name: Verify delete idempotency
ansible.builtin.assert:
that:
- result is not changed

View file

@ -0,0 +1,3 @@
cloud/hcloud
gather_facts/no
azp/group2

View file

@ -0,0 +1,35 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
# Azure Pipelines will configure this value to something similar to
# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i"
hcloud_prefix: "tests"
# Used to namespace resources created by concurrent test pipelines/targets
hcloud_run_ns: "{{ hcloud_prefix | md5 }}"
hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}"
hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}"
# Used to easily update the server types and images across all our tests.
hcloud_server_type_name: cax11
hcloud_server_type_id: 45
hcloud_server_type_upgrade_name: cax21
hcloud_server_type_upgrade_id: 93
hcloud_image_name: debian-12
hcloud_image_id: 114690389 # architecture=arm
hcloud_location_name: hel1
hcloud_location_id: 3
hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -0,0 +1,6 @@
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
hcloud_storage_box_name: "{{ hcloud_ns }}"
hcloud_ssh_key_name: "{{ hcloud_ns }}"
hcloud_storage_box_password: 1-secret-PASSW0RD-=)

View file

@ -0,0 +1,3 @@
---
dependencies:
- setup_ssh_keypair

View file

@ -0,0 +1,5 @@
---
- name: Cleanup test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
state: absent

View file

@ -0,0 +1,31 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
- name: Check if cleanup.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/cleanup.yml"
register: cleanup_file
- name: Check if prepare.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/prepare.yml"
register: prepare_file
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists
- name: Include prepare tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml"
when: prepare_file.stat.exists
- name: Run tests
block:
- name: Include test tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml"
always:
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists

View file

@ -0,0 +1,37 @@
---
- name: Create test_storage_box
hetzner.hcloud.storage_box:
name: "{{ hcloud_storage_box_name }}"
type: "{{ hcloud_storage_box_type_name }}"
location: "{{ hcloud_location_name }}"
password: "{{ hcloud_storage_box_password }}"
ssh_keys:
- "{{ test_ssh_keypair.public_key }}"
access_settings:
ssh_enabled: true
reachable_externally: false
labels:
key: "{{ hcloud_ns }}"
register: test_storage_box
- name: Create test_storage_box_subaccount1
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: subaccount1
description: backups from subaccount1
home_directory: backups/subaccount1
password: "{{ hcloud_storage_box_password }}"
access_settings:
ssh_enabled: true
readonly: false
labels:
key: "{{ hcloud_ns }}"
register: test_storage_box_subaccount1
- name: Create test_storage_box_subaccount2
hetzner.hcloud.storage_box_subaccount:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: subaccount2
home_directory: backups/subaccount2
password: "{{ hcloud_storage_box_password }}"
register: test_storage_box_subaccount2

View file

@ -0,0 +1,86 @@
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Gather hcloud_storage_box_subaccount_info
hetzner.hcloud.storage_box_subaccount_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
register: result
- name: Verify hcloud_storage_box_subaccount_info
ansible.builtin.assert:
that:
- result.hcloud_storage_box_subaccount_info | list | count == 2
- name: Gather hcloud_storage_box_subaccount_info in check mode
hetzner.hcloud.storage_box_subaccount_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
check_mode: true
register: result
- name: Verify hcloud_storage_box_subaccount_info in check mode
ansible.builtin.assert:
that:
- result.hcloud_storage_box_subaccount_info | list | count == 2
- name: Gather hcloud_storage_box_subaccount_info with label
hetzner.hcloud.storage_box_subaccount_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
label_selector: "key={{ hcloud_ns }}"
register: result
- name: Verify hcloud_storage_box_subaccount_info
ansible.builtin.assert:
that:
- result.hcloud_storage_box_subaccount_info | list | count == 1
- name: Gather hcloud_storage_box_subaccount_info with correct id
hetzner.hcloud.storage_box_subaccount_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
id: "{{ test_storage_box_subaccount1.hcloud_storage_box_subaccount.id }}"
register: result
- name: Verify hcloud_storage_box_subaccount_info with correct id
ansible.builtin.assert:
that:
- result.hcloud_storage_box_subaccount_info | list | count == 1
- result.hcloud_storage_box_subaccount_info[0].storage_box == test_storage_box.hcloud_storage_box.id
- result.hcloud_storage_box_subaccount_info[0].id is not none
- result.hcloud_storage_box_subaccount_info[0].name == "subaccount1"
- result.hcloud_storage_box_subaccount_info[0].home_directory == "backups/subaccount1"
- result.hcloud_storage_box_subaccount_info[0].username is not none
- result.hcloud_storage_box_subaccount_info[0].server is not none
- result.hcloud_storage_box_subaccount_info[0].description == "backups from subaccount1"
- result.hcloud_storage_box_subaccount_info[0].labels.key == hcloud_ns
- result.hcloud_storage_box_subaccount_info[0].access_settings.reachable_externally == false
- result.hcloud_storage_box_subaccount_info[0].access_settings.samba_enabled == false
- result.hcloud_storage_box_subaccount_info[0].access_settings.ssh_enabled == true
- result.hcloud_storage_box_subaccount_info[0].access_settings.webdav_enabled == false
- result.hcloud_storage_box_subaccount_info[0].access_settings.readonly == false
- result.hcloud_storage_box_subaccount_info[0].created is not none
- name: Gather hcloud_storage_box_subaccount_info with wrong id
hetzner.hcloud.storage_box_subaccount_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
id: "{{ test_storage_box_subaccount1.hcloud_storage_box_subaccount.id }}4321"
ignore_errors: true
register: result
- name: Verify hcloud_storage_box_subaccount_info with wrong id
ansible.builtin.assert:
that:
- result is failed
- name: Gather hcloud_storage_box_subaccount_info with correct name
hetzner.hcloud.storage_box_subaccount_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: "{{ test_storage_box_subaccount1.hcloud_storage_box_subaccount.name }}"
register: result
- name: Verify hcloud_storage_box_subaccount_info with correct name
ansible.builtin.assert:
that:
- result.hcloud_storage_box_subaccount_info | list | count == 1
- name: Gather hcloud_storage_box_subaccount_info with wrong name
hetzner.hcloud.storage_box_subaccount_info:
storage_box: "{{ test_storage_box.hcloud_storage_box.id }}"
name: "{{ test_storage_box_subaccount1.hcloud_storage_box_subaccount.name }}-invalid"
register: result
- name: Verify hcloud_storage_box_subaccount_info with wrong name
ansible.builtin.assert:
that:
- result.hcloud_storage_box_subaccount_info | list | count == 0

View file

@ -0,0 +1,3 @@
cloud/hcloud
gather_facts/no
azp/group2

View file

@ -0,0 +1,35 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
# Azure Pipelines will configure this value to something similar to
# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i"
hcloud_prefix: "tests"
# Used to namespace resources created by concurrent test pipelines/targets
hcloud_run_ns: "{{ hcloud_prefix | md5 }}"
hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}"
hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}"
# Used to easily update the server types and images across all our tests.
hcloud_server_type_name: cax11
hcloud_server_type_id: 45
hcloud_server_type_upgrade_name: cax21
hcloud_server_type_upgrade_id: 93
hcloud_image_name: debian-12
hcloud_image_id: 114690389 # architecture=arm
hcloud_location_name: hel1
hcloud_location_id: 3
hcloud_datacenter_name: hel1-dc2
hcloud_datacenter_id: 3
hcloud_network_zone_name: eu-central
hcloud_storage_box_type_name: bx11
hcloud_storage_box_type_id: 1333
hcloud_storage_box_type_upgrade_name: bx21
hcloud_storage_box_type_upgrade_id: 1334

View file

@ -0,0 +1,3 @@
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---

View file

@ -0,0 +1,31 @@
#
# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead.
#
---
- name: Check if cleanup.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/cleanup.yml"
register: cleanup_file
- name: Check if prepare.yml exists
ansible.builtin.stat:
path: "{{ role_path }}/tasks/prepare.yml"
register: prepare_file
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists
- name: Include prepare tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml"
when: prepare_file.stat.exists
- name: Run tests
block:
- name: Include test tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml"
always:
- name: Include cleanup tasks
ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml"
when: cleanup_file.stat.exists

View file

@ -0,0 +1,64 @@
# Copyright: (c) 2025, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Gather hcloud_storage_box_type_info
hetzner.hcloud.storage_box_type_info:
register: result
- name: Verify hcloud_storage_box_type_info
ansible.builtin.assert:
that:
- result.hcloud_storage_box_type_info | list | count >= 1
- name: Gather hcloud_storage_box_type_info in check mode
hetzner.hcloud.storage_box_type_info:
check_mode: true
register: result
- name: Verify hcloud_storage_box_type_info in check mode
ansible.builtin.assert:
that:
- result.hcloud_storage_box_type_info | list | count >= 1
- name: Gather hcloud_storage_box_type_info with correct id
hetzner.hcloud.storage_box_type_info:
id: "{{ hcloud_storage_box_type_id }}"
register: result
- name: Verify hcloud_storage_box_type_info with correct id
ansible.builtin.assert:
that:
- result.hcloud_storage_box_type_info | list | count == 1
- result.hcloud_storage_box_type_info[0].id == hcloud_storage_box_type_id
- result.hcloud_storage_box_type_info[0].name == hcloud_storage_box_type_name
- result.hcloud_storage_box_type_info[0].description == "BX11"
- result.hcloud_storage_box_type_info[0].snapshot_limit == 10
- result.hcloud_storage_box_type_info[0].automatic_snapshot_limit == 10
- result.hcloud_storage_box_type_info[0].subaccounts_limit == 100
- result.hcloud_storage_box_type_info[0].size == 1099511627776
- result.hcloud_storage_box_type_info[0].deprecation is none
- name: Gather hcloud_storage_box_type_info with wrong id
hetzner.hcloud.storage_box_type_info:
id: "{{ hcloud_storage_box_type_id }}4321"
ignore_errors: true
register: result
- name: Verify hcloud_storage_box_type_info with wrong id
ansible.builtin.assert:
that:
- result is failed
- name: Gather hcloud_storage_box_type_info with correct name
hetzner.hcloud.storage_box_type_info:
name: "{{ hcloud_storage_box_type_name }}"
register: result
- name: Verify hcloud_storage_box_type_info with correct name
ansible.builtin.assert:
that:
- result.hcloud_storage_box_type_info | list | count == 1
- name: Gather hcloud_storage_box_type_info with wrong name
hetzner.hcloud.storage_box_type_info:
name: "{{ hcloud_storage_box_type_name }}-invalid"
register: result
- name: Verify hcloud_storage_box_type_info with wrong name
ansible.builtin.assert:
that:
- result.hcloud_storage_box_type_info | list | count == 0

Some files were not shown because too many files have changed in this diff Show more