Skip to content

Storagebox cleanup #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions changelogs/fragments/173-storagebox.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
deprecated_features:
- "storagebox* modules - the ``hetzner_user`` and ``hetzner_pass`` options for these modules are deprecated; support will be removed in community.hrobot 3.0.0. Use ``hetzner_token`` instead (https://github.com/ansible-collections/community.hrobot/pull/173)."
- "storagebox* modules - the ``hetzner_token`` option for these modules will be required from community.hrobot 3.0.0 on (https://github.com/ansible-collections/community.hrobot/pull/173)."
- "storagebox* modules - membership in the ``community.hrobot.robot`` action group (module defaults group) is deprecated; the modules will be removed from the group in community.hrobot 3.0.0. Use ``community.hrobot.api`` instead (https://github.com/ansible-collections/community.hrobot/pull/173)."
- "storagebox_info - the ``storageboxes[].login``, ``storageboxes[].disk_quota``, ``storageboxes[].disk_usage``, ``storageboxes[].disk_usage_data``, ``storageboxes[].disk_usage_snapshot``, ``storageboxes[].webdav``, ``storageboxes[].samba``, ``storageboxes[].ssh``, ``storageboxes[].external_reachability``, and ``storageboxes[].zfs`` return values are deprecated and will be removed from community.routeros. Check out the documentation to find out their new names according to the new API (https://github.com/ansible-collections/community.hrobot/pull/173)."
- "storagebox_snapshot_info - the ``snapshots[].timestamp``, ``snapshots[].size``, ``snapshots[].filesystem_size``, ``snapshots[].automatic``, and ``snapshots[].comment`` return values are deprecated and will be removed from community.routeros. Check out the documentation to find out their new names according to the new API (https://github.com/ansible-collections/community.hrobot/pull/173)."
- "storagebox_snapshot_plan - the ``plans[].month`` return value is deprecated, since it only returns ``null`` with the new API and cannot be set to any other value (https://github.com/ansible-collections/community.hrobot/pull/173)."
- "storagebox_snapshot_plan_info - the ``plans[].month`` return value is deprecated, since it only returns ``null`` with the new API and cannot be set to any other value (https://github.com/ansible-collections/community.hrobot/pull/173)."
- "storagebox_subaccount - the ``subaccount.homedirectory``, ``subaccount.samba``, ``subaccount.ssh``, ``subaccount.external_reachability``, ``subaccount.webdav``, ``subaccount.readonly``, ``subaccount.createtime``, and ``subaccount.comment`` return values are deprecated and will be removed from community.routeros. Check out the documentation to find out their new names according to the new API (https://github.com/ansible-collections/community.hrobot/pull/173)."
- "storagebox_subaccount_info - the ``subaccounts[].accountid``, ``subaccounts[].homedirectory``, ``subaccounts[].samba``, ``subaccounts[].ssh``, ``subaccounts[].external_reachability``, ``subaccounts[].webdav``, ``subaccounts[].readonly``, ``subaccounts[].createtime``, and ``subaccounts[].comment`` return values are deprecated and will be removed from community.routeros. Check out the documentation to find out their new names according to the new API (https://github.com/ansible-collections/community.hrobot/pull/173)."
minor_changes:
- "storagebox* modules - the code for the old API (that has been removed by Hetzner) has been replaced by hard-coding the result of the API, namely that no storagebox of this ID exists (https://github.com/ansible-collections/community.hrobot/pull/173)."
24 changes: 24 additions & 0 deletions plugins/doc_fragments/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,27 @@ class ModuleDocFragment(object):
- If O(hetzner_password) is specified, O(hetzner_user) must also be specified, and O(hetzner_token) must not be specified.
required: false
"""

# Only for transition period
_ROBOT_COMPAT_SHIM_DEPRECATION = r"""
options:
hetzner_token:
description:
- The API token for the Robot web-service user.
- One of O(hetzner_token) and O(hetzner_user) must be specified.
- This option will be required from community.hrobot 3.0.0 on.
required: false
hetzner_user:
description:
- The username for the Robot web-service user.
- One of O(hetzner_token) and O(hetzner_user) must be specified.
- If O(hetzner_user) is specified, O(hetzner_password) must also be specified, and O(hetzner_token) must not be specified.
- This option is deprecated for this module, and support will be removed in community.hrobot 3.0.0.
required: false
hetzner_password:
description:
- The password for the Robot web-service user.
- If O(hetzner_password) is specified, O(hetzner_user) must also be specified, and O(hetzner_token) must not be specified.
- This option is deprecated for this module, and support will be removed in community.hrobot 3.0.0.
required: false
"""
14 changes: 14 additions & 0 deletions plugins/doc_fragments/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ class ModuleDocFragment(object):
- community.hrobot.robot
'''

# Only for transition period
_ACTIONGROUP_ROBOT_AND_API_DEPRECATION = r'''
options: {}
attributes:
action_group:
description:
- Use C(group/community.hrobot.robot) or C(group/community.hrobot.api) in C(module_defaults) to set defaults for this module.
- The C(group/community.hrobot.robot) group is B(deprecated) for this module; the module will be removed from the group in community.hrobot 3.0.0.
support: full
membership:
- community.hrobot.api
- community.hrobot.robot
'''

CONN = r"""
options: {}
attributes:
Expand Down
39 changes: 39 additions & 0 deletions plugins/module_utils/_tagging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-

# Copyright (c) 2025 Felix Fontein <[email protected]>
# This code is licensed under the following two licenses:
# - Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
# - GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: BSD-2-Clause OR GPL-3.0-or-later

# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!

from __future__ import absolute_import, division, print_function
__metaclass__ = type


try:
from ansible.module_utils.datatag import deprecate_value as _deprecate_value
HAS_DEPRECATE_VALUE = True
except ImportError:
HAS_DEPRECATE_VALUE = False


def deprecate_value(value, msg, version, help_text=None):
"""
Given a value, tag it as deprecated (with message, removal version, and optional help text).

For ansible-core versions that do not support data tagging, simply returns the value as-is.
"""
if not HAS_DEPRECATE_VALUE:
return value
# Assign this to a variable to work around a bug in ansible-test's pylint check (https://github.com/ansible/ansible/issues/85614)
collection_name = "community.hrobot"
return _deprecate_value(
value,
msg,
collection_name=collection_name,
version=version,
help_text=help_text,
)
6 changes: 6 additions & 0 deletions plugins/module_utils/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
hetzner_password=dict(type='str', required=False, no_log=True),
)

_ROBOT_DEFAULT_ARGUMENT_SPEC_COMPAT_DEPRECATED = dict(
hetzner_user=dict(type='str', required=False, removed_in_version="3.0.0", removed_from_collection="community.hrobot"),
hetzner_password=dict(type='str', required=False, no_log=True, removed_in_version="3.0.0", removed_from_collection="community.hrobot"),
)


# The API endpoint is fixed.
BASE_URL = "https://robot-ws.your-server.de"

Expand Down
182 changes: 69 additions & 113 deletions plugins/modules/storagebox.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
description:
- Modify a storage box's basic configuration.
extends_documentation_fragment:
- community.hrobot.api._robot_compat_shim # must come before api and robot
- community.hrobot.api._robot_compat_shim_deprecation # must come before api and robot
- community.hrobot.api
- community.hrobot.robot
- community.hrobot.attributes
- community.hrobot.attributes._actiongroup_robot_and_api # must come before the other two!
- community.hrobot.attributes._actiongroup_robot_and_api_deprecation # must come before the other two!
- community.hrobot.attributes.actiongroup_api
- community.hrobot.attributes.actiongroup_robot
attributes:
Expand Down Expand Up @@ -122,13 +122,10 @@

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six.moves.urllib.parse import urlencode

from ansible_collections.community.hrobot.plugins.module_utils.robot import (
BASE_URL,
ROBOT_DEFAULT_ARGUMENT_SPEC,
_ROBOT_DEFAULT_ARGUMENT_SPEC_COMPAT,
fetch_url_json,
_ROBOT_DEFAULT_ARGUMENT_SPEC_COMPAT_DEPRECATED,
)

from ansible_collections.community.hrobot.plugins.module_utils.api import (
Expand All @@ -141,15 +138,6 @@
)


PARAMETERS_LEGACY = {
'name': ('name', 'storagebox_name'),
'webdav': ('webdav', 'webdav'),
'samba': ('samba', 'samba'),
'ssh': ('ssh', 'ssh'),
'external_reachability': ('external_reachability', 'external_reachability'),
'zfs': ('zfs', 'zfs'),
}

UPDATE_PARAMETERS = {
'name': ('name', ['name'], 'name'),
}
Expand All @@ -166,11 +154,6 @@
PARAMETERS.update(ACTION_PARAMETERS)


def extract_legacy(result):
sb = result['storagebox']
return {key: sb.get(key) for key, dummy in PARAMETERS_LEGACY.values()}


def extract(result):
sb = result['storage_box']

Expand All @@ -194,7 +177,7 @@ def main():
zfs=dict(type='bool'),
)
argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
argument_spec.update(_ROBOT_DEFAULT_ARGUMENT_SPEC_COMPAT)
argument_spec.update(_ROBOT_DEFAULT_ARGUMENT_SPEC_COMPAT_DEPRECATED)
argument_spec.update(API_DEFAULT_ARGUMENT_SPEC)
argument_spec.update(_API_DEFAULT_ARGUMENT_SPEC_COMPAT)
module = AnsibleModule(
Expand All @@ -210,102 +193,75 @@ def main():
after = {}
changes = {}

if module.params["hetzner_token"] is None:
module.deprecate(
"The hetzner_token parameter will be required from community.hrobot 3.0.0 on.",
collection_name="community.hrobot",
version="3.0.0",
)
if module.params["hetzner_user"] is not None:
# DEPRECATED: old API
url = "{0}/storagebox/{1}".format(BASE_URL, storagebox_id)
result, error = fetch_url_json(module, url, accept_errors=['STORAGEBOX_NOT_FOUND'])
if error:
module.fail_json(msg='Storagebox with ID {0} does not exist'.format(storagebox_id))

before = extract_legacy(result)
after = dict(before)

for option_name, (data_name, change_name) in PARAMETERS_LEGACY.items():
value = module.params[option_name]
if value is not None:
if before[data_name] != value:
after[data_name] = value
if isinstance(value, bool):
changes[change_name] = str(value).lower()
else:
changes[change_name] = value

if changes and not module.check_mode:
headers = {"Content-type": "application/x-www-form-urlencoded"}
result, error = fetch_url_json(
module,
url,
data=urlencode(changes),
headers=headers,
method='POST',
accept_errors=['INVALID_INPUT'],
)
if error:
invalid = result['error'].get('invalid') or []
module.fail_json(msg='The values to update were invalid ({0})'.format(', '.join(invalid)))
after = extract_legacy(result)

else:
# NEW API!
url = "{0}/v1/storage_boxes/{1}".format(API_BASE_URL, storagebox_id)
result, dummy, error = api_fetch_url_json(module, url, accept_errors=['not_found'])
module.warn("The old storagebox API has been disabled by Hetzner. The supporting code has been removed.")
module.fail_json(msg='Storagebox with ID {0} does not exist'.format(storagebox_id))

url = "{0}/v1/storage_boxes/{1}".format(API_BASE_URL, storagebox_id)
result, dummy, error = api_fetch_url_json(module, url, accept_errors=['not_found'])
if error:
module.fail_json(msg='Storagebox with ID {0} does not exist'.format(storagebox_id))

before = extract(result)
after = dict(before)

update = {}
for option_name, (data_name, dummy, change_name) in UPDATE_PARAMETERS.items():
value = module.params[option_name]
if value is not None:
if before[data_name] != value:
after[data_name] = value
changes[change_name] = value
update[change_name] = value

action = {}
update_after_update = {}
for option_name, (data_name, dummy, change_name) in ACTION_PARAMETERS.items():
value = module.params[option_name]
if value is not None:
if before[data_name] != value:
after[data_name] = value
update_after_update[data_name] = value
changes[change_name] = value
action[change_name] = value

if update and not module.check_mode:
headers = {"Content-type": "application/json"}
result, dummy, error = api_fetch_url_json(
module,
url,
data=module.jsonify(update),
headers=headers,
method='PUT',
accept_errors=['invalid_input'],
)
if error:
module.fail_json(msg='Storagebox with ID {0} does not exist'.format(storagebox_id))

before = extract(result)
after = dict(before)

update = {}
for option_name, (data_name, dummy, change_name) in UPDATE_PARAMETERS.items():
value = module.params[option_name]
if value is not None:
if before[data_name] != value:
after[data_name] = value
changes[change_name] = value
update[change_name] = value

action = {}
update_after_update = {}
for option_name, (data_name, dummy, change_name) in ACTION_PARAMETERS.items():
value = module.params[option_name]
if value is not None:
if before[data_name] != value:
after[data_name] = value
update_after_update[data_name] = value
changes[change_name] = value
action[change_name] = value

if update and not module.check_mode:
headers = {"Content-type": "application/json"}
result, dummy, error = api_fetch_url_json(
details = result['error'].get('details') or {}
fields = details.get("fields") or []
details_str = ", ".join(['{0}: {1}'.format(to_native(field["name"]), to_native(field["message"])) for field in fields])
module.fail_json(msg='The values to update were invalid ({0})'.format(details_str or "no details"))
after = extract(result)

if action and not module.check_mode:
after.update(update_after_update)
action_url = "{0}/actions/update_access_settings".format(url)
try:
api_apply_action(
module,
url,
data=module.jsonify(update),
headers=headers,
method='PUT',
accept_errors=['invalid_input'],
action_url,
action,
lambda action_id: "{0}/v1/storage_boxes/actions/{1}".format(API_BASE_URL, action_id),
check_done_delay=1,
check_done_timeout=60,
)
if error:
details = result['error'].get('details') or {}
fields = details.get("fields") or []
details_str = ", ".join(['{0}: {1}'.format(to_native(field["name"]), to_native(field["message"])) for field in fields])
module.fail_json(msg='The values to update were invalid ({0})'.format(details_str or "no details"))
after = extract(result)

if action and not module.check_mode:
after.update(update_after_update)
action_url = "{0}/actions/update_access_settings".format(url)
try:
api_apply_action(
module,
action_url,
action,
lambda action_id: "{0}/v1/storage_boxes/actions/{1}".format(API_BASE_URL, action_id),
check_done_delay=1,
check_done_timeout=60,
)
except ApplyActionError as exc:
module.fail_json(msg='Error while updating access settings: {0}'.format(exc))
except ApplyActionError as exc:
module.fail_json(msg='Error while updating access settings: {0}'.format(exc))

result = dict(after)
result['changed'] = bool(changes)
Expand Down
Loading
Loading