diff --git a/changelogs/fragments/173-storagebox.yml b/changelogs/fragments/173-storagebox.yml new file mode 100644 index 00000000..96f63d71 --- /dev/null +++ b/changelogs/fragments/173-storagebox.yml @@ -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)." diff --git a/plugins/doc_fragments/api.py b/plugins/doc_fragments/api.py index 7af3559e..6449cdeb 100644 --- a/plugins/doc_fragments/api.py +++ b/plugins/doc_fragments/api.py @@ -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 +""" diff --git a/plugins/doc_fragments/attributes.py b/plugins/doc_fragments/attributes.py index 7753f50b..affb1ee0 100644 --- a/plugins/doc_fragments/attributes.py +++ b/plugins/doc_fragments/attributes.py @@ -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: diff --git a/plugins/module_utils/_tagging.py b/plugins/module_utils/_tagging.py new file mode 100644 index 00000000..39d59ce3 --- /dev/null +++ b/plugins/module_utils/_tagging.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2025 Felix Fontein +# 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, + ) diff --git a/plugins/module_utils/robot.py b/plugins/module_utils/robot.py index cd953da8..e88d543d 100644 --- a/plugins/module_utils/robot.py +++ b/plugins/module_utils/robot.py @@ -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" diff --git a/plugins/modules/storagebox.py b/plugins/modules/storagebox.py index 4c8e2fce..1b4f0646 100644 --- a/plugins/modules/storagebox.py +++ b/plugins/modules/storagebox.py @@ -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: @@ -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 ( @@ -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'), } @@ -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'] @@ -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( @@ -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) diff --git a/plugins/modules/storagebox_info.py b/plugins/modules/storagebox_info.py index 70aa0bc4..34ff5a05 100644 --- a/plugins/modules/storagebox_info.py +++ b/plugins/modules/storagebox_info.py @@ -18,11 +18,11 @@ description: - Query information on one or more storage box. 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 - community.hrobot.attributes.idempotent_not_modify_state @@ -88,6 +88,8 @@ description: - The storage box's login name. - Note that this is copied from RV(storageboxes[].username) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: str sample: u12345 returned: success @@ -200,6 +202,8 @@ description: - Total amount of MB available. - Note that this is copied from RV(storageboxes[].storage_box_type.size) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: int sample: 10240000 returned: when O(full_info=true), or O(hetzner_token) is specified @@ -207,6 +211,8 @@ description: - The amount of MB in use. - Note that this is copied from RV(storageboxes[].stats.size) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: int sample: 900 returned: when O(full_info=true), or O(hetzner_token) is specified @@ -214,6 +220,8 @@ description: - The amount of MB used by files. - Note that this is copied from RV(storageboxes[].stats.size_data) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: int sample: 500 returned: when O(full_info=true), or O(hetzner_token) is specified @@ -221,6 +229,8 @@ description: - The amount of MB used by snapshots. - Note that this is copied from RV(storageboxes[].stats.size_snapshots) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: int sample: 400 returned: when O(full_info=true), or O(hetzner_token) is specified @@ -228,6 +238,8 @@ description: - Whether WebDAV is active. - Note that this is copied from RV(storageboxes[].access_settings.webdav_enabled) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: true returned: when O(full_info=true), or O(hetzner_token) is specified @@ -235,6 +247,8 @@ description: - Whether SAMBA is active. - Note that this is copied from RV(storageboxes[].access_settings.samba_enabled) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: true returned: when O(full_info=true), or O(hetzner_token) is specified @@ -242,6 +256,8 @@ description: - Whether SSH is active. - Note that this is copied from RV(storageboxes[].access_settings.ssh_enabled) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: true returned: when O(full_info=true), or O(hetzner_token) is specified @@ -249,6 +265,8 @@ description: - Whether the storage box is reachable externally. - Note that this is copied from RV(storageboxes[].access_settings.reachable_externally) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: true returned: when O(full_info=true), or O(hetzner_token) is specified @@ -256,6 +274,8 @@ description: - Shows whether the ZFS directory is visible. - Note that this is copied from RV(storageboxes[].access_settings.zfs_enabled) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: false returned: when O(full_info=true), or O(hetzner_token) is specified @@ -517,13 +537,10 @@ """ from ansible.module_utils.basic import AnsibleModule -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 ( @@ -534,6 +551,10 @@ api_fetch_url_json_list, ) +from ansible_collections.community.hrobot.plugins.module_utils._tagging import ( + deprecate_value, +) + _CONVERT = { "login": ["username"], @@ -555,7 +576,11 @@ def add_hrobot_compat_shim(storagebox): value = storagebox for src in source: value = value[src] - result[dest] = value + result[dest] = deprecate_value( + value, + "The return value `{0}` is deprecated; use `{1}` instead.".format(dest, ".".join(source)), + version="3.0.0", + ) return result @@ -566,7 +591,7 @@ def main(): full_info=dict(type='bool', default=False), ) 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( @@ -582,50 +607,25 @@ def main(): full_info = module.params['full_info'] storageboxes = [] + 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 - if storagebox_id is not None: - storagebox_ids = [storagebox_id] - else: - url = "{0}/storagebox".format(BASE_URL) - data = None - headers = None - if linked_server_number is not None: - data = urlencode({ - "linked_server": linked_server_number, - }) - headers = { - "Content-type": "application/x-www-form-urlencoded", - } - result, error = fetch_url_json(module, url, accept_errors=['STORAGEBOX_NOT_FOUND'], data=data) - storagebox_ids = [] - if not error: - # When filtering by linked_server, the result should be a dictionary - if isinstance(result, dict): - result = [result] - for entry in result: - if full_info: - storagebox_ids.append(entry['storagebox']['id']) - else: - storageboxes.append(entry['storagebox']) - - for storagebox_id in storagebox_ids: - url = "{0}/storagebox/{1}".format(BASE_URL, storagebox_id) - result, error = fetch_url_json(module, url, accept_errors=['STORAGEBOX_NOT_FOUND']) - if not error: - storageboxes.append(result['storagebox']) + module.warn("The old storagebox API has been disabled by Hetzner. The supporting code has been removed.") + module.exit_json(changed=False, storageboxes=[]) + if storagebox_id is not None: + 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 is None: + storageboxes = [result["storage_box"]] else: - # NEW API! - if storagebox_id is not None: - 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 is None: - storageboxes = [result["storage_box"]] - else: - url = "{0}/v1/storage_boxes".format(API_BASE_URL) - storageboxes, dummy = api_fetch_url_json_list(module, url, data_key="storage_boxes") - storageboxes = [add_hrobot_compat_shim(storagebox) for storagebox in storageboxes] + url = "{0}/v1/storage_boxes".format(API_BASE_URL) + storageboxes, dummy = api_fetch_url_json_list(module, url, data_key="storage_boxes") + storageboxes = [add_hrobot_compat_shim(storagebox) for storagebox in storageboxes] module.exit_json( changed=False, diff --git a/plugins/modules/storagebox_set_password.py b/plugins/modules/storagebox_set_password.py index 2f55c68e..232a084d 100644 --- a/plugins/modules/storagebox_set_password.py +++ b/plugins/modules/storagebox_set_password.py @@ -17,11 +17,11 @@ description: - (Re)set the password for a storage box. 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 @@ -81,13 +81,10 @@ """ from ansible.module_utils.basic import AnsibleModule -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 ( @@ -105,7 +102,7 @@ def main(): password=dict(type="str", no_log=True), ) 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( @@ -117,52 +114,37 @@ def main(): id = module.params["id"] password = module.params.get("password") + 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}/password".format(BASE_URL, id) - accepted_errors = ["STORAGEBOX_NOT_FOUND", "STORAGEBOX_INVALID_PASSWORD"] - - if password: - headers = {"Content-type": "application/x-www-form-urlencoded"} - result, error = fetch_url_json( - module, url, method="POST", accept_errors=accepted_errors, data=urlencode({"password": password}), headers=headers) - else: - result, error = fetch_url_json( - module, url, method="POST", accept_errors=accepted_errors) - - if error == 'STORAGEBOX_NOT_FOUND': - module.fail_json( - msg='Storage Box with ID {0} not found'.format(id)) - - if error == 'STORAGEBOX_INVALID_PASSWORD': - module.fail_json( - msg="The chosen password has been considered insecure or does not comply with Hetzner's password guideline") - - module.exit_json(changed=True, password=result["password"]) - - else: - # NEW API! - action_url = "{0}/v1/storage_boxes/{1}/actions/reset_password".format(API_BASE_URL, id) - action = { - "password": password, - } - try: - dummy, error = 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, - accept_errors=["not_found"], - ) - except ApplyActionError as exc: - module.fail_json(msg='Error while resetting password: {0}'.format(exc)) - - if error == "not_found": - module.fail_json(msg='Storage Box with ID {0} not found'.format(id)) - - module.exit_json(changed=True, password=password) + module.warn("The old storagebox API has been disabled by Hetzner. The supporting code has been removed.") + module.fail_json(msg='Storage Box with ID {0} not found'.format(id)) + + action_url = "{0}/v1/storage_boxes/{1}/actions/reset_password".format(API_BASE_URL, id) + action = { + "password": password, + } + try: + dummy, error = 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, + accept_errors=["not_found"], + ) + except ApplyActionError as exc: + module.fail_json(msg='Error while resetting password: {0}'.format(exc)) + + if error == "not_found": + module.fail_json(msg='Storage Box with ID {0} not found'.format(id)) + + module.exit_json(changed=True, password=password) if __name__ == '__main__': # pragma: no cover diff --git a/plugins/modules/storagebox_snapshot.py b/plugins/modules/storagebox_snapshot.py index f16e2c9d..88904e1a 100644 --- a/plugins/modules/storagebox_snapshot.py +++ b/plugins/modules/storagebox_snapshot.py @@ -18,11 +18,11 @@ description: - Create, update comment, or delete a snapshot of a storage box. 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: @@ -108,10 +108,8 @@ 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 ( @@ -124,15 +122,6 @@ ) -def legacy_handle_errors(module, error, storagebox_id=None, snapshot_name=None): - error_messages = { - "STORAGEBOX_NOT_FOUND": "Storagebox with ID {0} does not exist".format(storagebox_id), - "SNAPSHOT_NOT_FOUND": "Snapshot with name {0} does not exist".format(snapshot_name), - "SNAPSHOT_LIMIT_EXCEEDED": "Snapshot limit exceeded", - } - module.fail_json(msg=error_messages.get(error, error)) - - def extract_legacy(snapshot): return { 'id': snapshot['id'], @@ -153,7 +142,7 @@ def main(): snapshot_comment=dict(type='str') ) 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) @@ -168,166 +157,92 @@ def main(): snapshot_name = module.params['snapshot_name'] snapshot_comment = module.params['snapshot_comment'] + 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 - - # Create snapshot - if state == 'present' and not snapshot_name: - if module.check_mode: - module.exit_json(changed=True) - snapshot = legacy_create_snapshot(module, storagebox_id) - - # Add the comment if provided - if snapshot_comment is not None: - legacy_update_snapshot_comment(module, storagebox_id, snapshot['name'], snapshot_comment) - snapshot['comment'] = snapshot_comment + 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)) - module.exit_json(changed=True, snapshot=snapshot) - - # Update snapshot comment - elif state == 'present' and snapshot_name: - if snapshot_comment is None: - module.fail_json(msg="snapshot_comment is required when updating a snapshot") - - snapshots = legacy_fetch_snapshots(module=module, storagebox_id=storagebox_id) - snapshot = legacy_get_snapshot_by_name(snapshots, snapshot_name) - if not snapshot: - legacy_handle_errors(module, "SNAPSHOT_NOT_FOUND", snapshot_name=snapshot_name) - if snapshot_comment != snapshot['comment']: - if not module.check_mode: - legacy_update_snapshot_comment(module, storagebox_id, snapshot_name, snapshot_comment) - module.exit_json(changed=True, snapshot=snapshot) - else: - module.exit_json(changed=False, snapshot=snapshot) - - # Delete snapshot + # Create snapshot + if state == 'present' and not snapshot_name: + if module.check_mode: + module.exit_json(changed=True) + action_url = "{0}/v1/storage_boxes/{1}/snapshots".format(API_BASE_URL, storagebox_id) + action = {} + if snapshot_comment: + action["description"] = snapshot_comment + try: + extracted_ids, error = 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=120, + accept_errors=["not_found"], + ) + except ApplyActionError as exc: + module.fail_json(msg='Error while creating snapshot: {0}'.format(exc)) + + if error == "not_found": + module.fail_json(msg="Storagebox with ID {0} does not exist".format(storagebox_id)) + + new_snapshot_id = extracted_ids["storage_box_snapshot"] + # Retrieve created snapshot + url = "{0}/v1/storage_boxes/{1}/snapshots/{2}".format(API_BASE_URL, storagebox_id, new_snapshot_id) + snapshot = api_fetch_url_json(module, url, method='GET')[0]["snapshot"] + + module.exit_json(changed=True, snapshot=extract_legacy(snapshot)) + + # Update snapshot comment + elif state == 'present' and snapshot_name: + if snapshot_comment is None: + module.fail_json(msg="snapshot_comment is required when updating a snapshot") + + snapshot = find_snapshot(module, storagebox_id, snapshot_name) + if not snapshot: + module.fail_json(msg="Snapshot with name {0} does not exist".format(snapshot_name)) + if snapshot_comment == snapshot['description']: + module.exit_json(changed=False, snapshot=extract_legacy(snapshot)) + if not module.check_mode: + url = "{0}/v1/storage_boxes/{1}/snapshots/{2}".format(API_BASE_URL, storagebox_id, snapshot['id']) + headers = {"Content-type": "application/json"} + result, dummy, dummy2 = api_fetch_url_json( + module, + url, + method='PUT', + data=module.jsonify({"description": snapshot_comment}), + headers=headers, + ) + snapshot = result["snapshot"] else: - snapshots = legacy_fetch_snapshots(module=module, storagebox_id=storagebox_id) - snapshot = legacy_get_snapshot_by_name(snapshots, snapshot_name) - if snapshot: - if not module.check_mode: - legacy_delete_snapshot(module, storagebox_id, snapshot_name) - module.exit_json(changed=True) - else: - module.exit_json(changed=False) + snapshot['description'] = snapshot_comment + module.exit_json(changed=True, snapshot=extract_legacy(snapshot)) + # Delete snapshot else: - # NEW API! - - # Create snapshot - if state == 'present' and not snapshot_name: - if module.check_mode: - module.exit_json(changed=True) - action_url = "{0}/v1/storage_boxes/{1}/snapshots".format(API_BASE_URL, storagebox_id) - action = {} - if snapshot_comment: - action["description"] = snapshot_comment + snapshot = find_snapshot(module, storagebox_id, snapshot_name) + if not snapshot: + module.exit_json(changed=False) + if not module.check_mode: + action_url = "{0}/v1/storage_boxes/{1}/snapshots/{2}".format(API_BASE_URL, storagebox_id, snapshot['id']) try: - extracted_ids, error = api_apply_action( + api_apply_action( module, action_url, - action, + None, lambda action_id: "{0}/v1/storage_boxes/actions/{1}".format(API_BASE_URL, action_id), + method='DELETE', check_done_delay=1, check_done_timeout=120, - accept_errors=["not_found"], ) except ApplyActionError as exc: - module.fail_json(msg='Error while creating snapshot: {0}'.format(exc)) - - if error == "not_found": - module.fail_json(msg="Storagebox with ID {0} does not exist".format(storagebox_id)) - - new_snapshot_id = extracted_ids["storage_box_snapshot"] - # Retrieve created snapshot - url = "{0}/v1/storage_boxes/{1}/snapshots/{2}".format(API_BASE_URL, storagebox_id, new_snapshot_id) - snapshot = api_fetch_url_json(module, url, method='GET')[0]["snapshot"] - - module.exit_json(changed=True, snapshot=extract_legacy(snapshot)) - - # Update snapshot comment - elif state == 'present' and snapshot_name: - if snapshot_comment is None: - module.fail_json(msg="snapshot_comment is required when updating a snapshot") - - snapshot = find_snapshot(module, storagebox_id, snapshot_name) - if not snapshot: - module.fail_json(msg="Snapshot with name {0} does not exist".format(snapshot_name)) - if snapshot_comment == snapshot['description']: - module.exit_json(changed=False, snapshot=extract_legacy(snapshot)) - if not module.check_mode: - url = "{0}/v1/storage_boxes/{1}/snapshots/{2}".format(API_BASE_URL, storagebox_id, snapshot['id']) - headers = {"Content-type": "application/json"} - result, dummy, dummy2 = api_fetch_url_json( - module, - url, - method='PUT', - data=module.jsonify({"description": snapshot_comment}), - headers=headers, - ) - snapshot = result["snapshot"] - else: - snapshot['description'] = snapshot_comment - module.exit_json(changed=True, snapshot=extract_legacy(snapshot)) - - # Delete snapshot - else: - snapshot = find_snapshot(module, storagebox_id, snapshot_name) - if not snapshot: - module.exit_json(changed=False) - if not module.check_mode: - action_url = "{0}/v1/storage_boxes/{1}/snapshots/{2}".format(API_BASE_URL, storagebox_id, snapshot['id']) - try: - api_apply_action( - module, - action_url, - None, - lambda action_id: "{0}/v1/storage_boxes/actions/{1}".format(API_BASE_URL, action_id), - method='DELETE', - check_done_delay=1, - check_done_timeout=120, - ) - except ApplyActionError as exc: - module.fail_json(msg='Error while deleting snapshot: {0}'.format(exc)) - module.exit_json(changed=True) - - -def legacy_delete_snapshot(module, storagebox_id, snapshot_name): - url = "{0}/storagebox/{1}/snapshot/{2}".format(BASE_URL, storagebox_id, snapshot_name) - fetch_url_json(module, url, method="DELETE", allow_empty_result=True) - - -def legacy_update_snapshot_comment(module, storagebox_id, snapshot_name, snapshot_comment): - url = "{0}/storagebox/{1}/snapshot/{2}/comment".format(BASE_URL, storagebox_id, snapshot_name) - headers = {"Content-type": "application/x-www-form-urlencoded"} - fetch_url_json( - module, url, method="POST", data=urlencode({"comment": snapshot_comment}), headers=headers, allow_empty_result=True, - ) - - -def legacy_create_snapshot(module, storagebox_id): - url = "{0}/storagebox/{1}/snapshot".format(BASE_URL, storagebox_id) - result, error = fetch_url_json( - module, url, method="POST", accept_errors=["STORAGEBOX_NOT_FOUND", "SNAPSHOT_LIMIT_EXCEEDED"], - ) - if error: - legacy_handle_errors(module, error, storagebox_id) - return result['snapshot'] - - -def legacy_get_snapshot_by_name(snapshots, name): - for snapshot in snapshots: - if snapshot['name'] == name: - return snapshot - return None - - -def legacy_fetch_snapshots(module, storagebox_id): - url = "{0}/storagebox/{1}/snapshot".format(BASE_URL, storagebox_id) - result, error = fetch_url_json(module, url, method="GET", accept_errors=["STORAGEBOX_NOT_FOUND"]) - if error: - legacy_handle_errors(module, error, storagebox_id) - return [item['snapshot'] for item in result] + module.fail_json(msg='Error while deleting snapshot: {0}'.format(exc)) + module.exit_json(changed=True) def find_snapshot(module, storagebox_id, snapshot_name): diff --git a/plugins/modules/storagebox_snapshot_info.py b/plugins/modules/storagebox_snapshot_info.py index a5a7906b..3010a37d 100644 --- a/plugins/modules/storagebox_snapshot_info.py +++ b/plugins/modules/storagebox_snapshot_info.py @@ -18,11 +18,11 @@ description: - Query the snapshots for a storage box. 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 - community.hrobot.attributes.idempotent_not_modify_state @@ -71,6 +71,8 @@ description: - The timestamp of snapshot in UTC. - Note that this is copied from RV(snapshots[].created) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: str sample: "2025-01-21T13:40:38+00:00" returned: success @@ -78,6 +80,8 @@ description: - The Snapshot size in MB. - Note that this is copied from RV(snapshots[].stats.size) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: int sample: 400 returned: success @@ -85,6 +89,8 @@ description: - The size of the Storage Box at creation time of the snapshot in MB. - Note that this is computed from RV(snapshots[].stats.size_filesystem) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: int sample: 12345 returned: success @@ -92,6 +98,8 @@ description: - Whether the snapshot was created automatically. - Note that this is computed from RV(snapshots[].is_automatic) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: false returned: success @@ -99,6 +107,8 @@ description: - The comment for the snapshot. - Note that this is copied from RV(snapshots[].description) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: str sample: "This is a snapshot" returned: success @@ -165,10 +175,8 @@ from ansible.module_utils.basic import AnsibleModule 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 ( @@ -178,14 +186,38 @@ api_fetch_url_json, ) +from ansible_collections.community.hrobot.plugins.module_utils._tagging import ( + deprecate_value, +) + def adjust_legacy(snapshot): result = dict(snapshot) - result["timestamp"] = result["created"] - result["size"] = result["stats"]["size"] // (1024 * 1024) - result["filesystem_size"] = result["stats"]["size_filesystem"] // (1024 * 1024) - result["automatic"] = result["is_automatic"] - result["comment"] = result["description"] + result["timestamp"] = deprecate_value( + result["created"], + "The return value `timestamp` is deprecated; use `created` instead.", + version="3.0.0", + ) + result["size"] = deprecate_value( + result["stats"]["size"] // (1024 * 1024), + "The return value `size` is deprecated; use `stats.size / (1024*1024)` instead.", + version="3.0.0", + ) + result["filesystem_size"] = deprecate_value( + result["stats"]["size_filesystem"] // (1024 * 1024), + "The return value `filesystem_size` is deprecated; use `stats.size_filesystem / (1024*1024)` instead.", + version="3.0.0", + ) + result["automatic"] = deprecate_value( + result["is_automatic"], + "The return value `automatic` is deprecated; use `is_automatic` instead.", + version="3.0.0", + ) + result["comment"] = deprecate_value( + result["description"], + "The return value `comment` is deprecated; use `description` instead.", + version="3.0.0", + ) return result @@ -194,7 +226,7 @@ def main(): storagebox_id=dict(type='int', required=True), ) 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( @@ -204,30 +236,25 @@ def main(): storagebox_id = module.params['storagebox_id'] - if module.params["hetzner_user"] is not None: - # DEPRECATED: old API - url = "{0}/storagebox/{1}/snapshot".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)) - - module.exit_json( - changed=False, - snapshots=[item['snapshot'] for item in result], + 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: + 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)) - else: - # NEW API! - - url = "{0}/v1/storage_boxes/{1}/snapshots".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)) + url = "{0}/v1/storage_boxes/{1}/snapshots".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)) - module.exit_json( - changed=False, - snapshots=[adjust_legacy(item) for item in result['snapshots']], - ) + module.exit_json( + changed=False, + snapshots=[adjust_legacy(item) for item in result['snapshots']], + ) if __name__ == '__main__': # pragma: no cover diff --git a/plugins/modules/storagebox_snapshot_plan.py b/plugins/modules/storagebox_snapshot_plan.py index e9a2bc02..2f97f19f 100644 --- a/plugins/modules/storagebox_snapshot_plan.py +++ b/plugins/modules/storagebox_snapshot_plan.py @@ -18,11 +18,11 @@ description: - Enable, modify, and disable the snapshot plans of a storage box. 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: @@ -155,6 +155,8 @@ - The month of execution of the plan. V(1) is January, V(12) is December. - If set to V(null), the plan is run every month. - Always V(null) if O(hetzner_token) is provided. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: int sample: null returned: success @@ -167,13 +169,10 @@ """ from ansible.module_utils.basic import AnsibleModule -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 ( @@ -185,21 +184,9 @@ api_fetch_url_json, ) - -LEGACY_PARAMETERS = { - 'status': ('status', 'status'), # according to the API docs 'status' cannot be provided as input to POST, but that's not true - 'minute': ('minute', 'minute'), - 'hour': ('hour', 'hour'), - 'day_of_week': ('day_of_week', 'day_of_week'), - 'day_of_month': ('day_of_month', 'day_of_month'), - 'month': ('month', 'month'), - 'max_snapshots': ('max_snapshots', 'max_snapshots'), -} - - -def extract_legacy(result): - sb = result['snapshotplan'] - return {key: sb.get(key) for key, dummy in LEGACY_PARAMETERS.values()} +from ansible_collections.community.hrobot.plugins.module_utils._tagging import ( + deprecate_value, +) def extract(result): @@ -212,7 +199,7 @@ def extract(result): 'hour': None, 'day_of_week': None, 'day_of_month': None, - 'month': None, + 'month': deprecate_value(None, "The return value `month` is deprecated; it is always null.", version="3.0.0"), 'max_snapshots': None, } @@ -222,7 +209,7 @@ def extract(result): 'hour': sp['hour'], 'day_of_week': sp['day_of_week'], 'day_of_month': sp['day_of_month'], - 'month': None, + 'month': deprecate_value(None, "The return value `month` is deprecated; it is always null.", version="3.0.0"), 'max_snapshots': sp['max_snapshots'], } @@ -249,7 +236,7 @@ def main(): ), ) 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( @@ -265,115 +252,51 @@ def main(): module.fail_json(msg='`plans` must have exactly one element') plan = plans[0] + 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}/snapshotplan".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)) + 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)) - # The documentation (https://robot.hetzner.com/doc/webservice/en.html#get-storagebox-storagebox-id-snapshotplan) - # claims that the result is a list, but actually it is a dictionary. Convert it to a list of dicts if that's the case. - if isinstance(result, dict): - result = [result] + if plans[0]['month'] is not None: + module.fail_json(msg='The new Hetzner API does not support specifying month for a plan.') - before = [extract_legacy(plan) for plan in result] - after = [ - { - data_name: ( - plan[option_name] - if plan['status'] == 'enabled' or option_name == 'status' else - None - ) - for option_name, (data_name, dummy) in LEGACY_PARAMETERS.items() - } - for plan in plans - ] - changes = [] + 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)) - for index, plan in enumerate(after): - existing_plan = before[index] if index < len(before) else {} - plan_values = {} - has_changes = False - for data_name, change_name in LEGACY_PARAMETERS.values(): - before_value = existing_plan.get(data_name) - after_value = plan[data_name] - if before_value != after_value: - has_changes = True - if after_value is not None and change_name is not None: - plan_values[change_name] = after_value - if has_changes: - if plan['status'] == 'disabled': - # For some reason, minute and hour are required even for disabled plans, - # even though the documentation says otherwise - plan_values['minute'] = 0 - plan_values['hour'] = 0 - changes.append((index, plan_values)) - - if changes and not module.check_mode: - headers = {"Content-type": "application/x-www-form-urlencoded"} - # TODO: If the API ever changes to support more than one plan, the following need to change - if len(changes) != 1: # pragma: no cover - raise AssertionError('Current implementation can handle only one plan') # pragma: no cover - actual_changes = changes[0][1] - result, error = fetch_url_json( + before = extract(result) + after = { + key: (value if plan['status'] == 'enabled' or key == 'status' else None) + for key, value in plan.items() + } + action_enable = None + if before != after: + action_enable = (after['status'] == 'enabled') + action = {key: value for key, value in plan.items() if key not in ('status', 'month')} if action_enable else {} + + if action_enable is not None and not module.check_mode: + action_url = "{0}/actions/{1}".format(url, 'enable_snapshot_plan' if action_enable else 'disable_snapshot_plan') + try: + api_apply_action( module, - url, - data=urlencode(actual_changes) if actual_changes else None, - headers=headers, - method='POST', - accept_errors=['INVALID_INPUT'], + action_url, + action if action_enable else None, + 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: - invalid = result['error'].get('invalid') or [] - module.fail_json(msg='The values to update were invalid ({0})'.format(', '.join(invalid))) - - # The documentation (https://robot.hetzner.com/doc/webservice/en.html#post-storagebox-storagebox-id-snapshotplan) - # claims that the result is a list, but actually it is a dictionary. Convert it to a list of dicts if that's the case. - if isinstance(result, dict): - result = [result] - - after = [extract_legacy(plan) for plan in result] - - changed = bool(changes) - - else: - # NEW API! - if plans[0]['month'] is not None: - module.fail_json(msg='The new Hetzner API does not support specifying month for a plan.') - - 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 = { - key: (value if plan['status'] == 'enabled' or key == 'status' else None) - for key, value in plan.items() - } - action_enable = None - if before != after: - action_enable = (after['status'] == 'enabled') - action = {key: value for key, value in plan.items() if key not in ('status', 'month')} if action_enable else {} - - if action_enable is not None and not module.check_mode: - action_url = "{0}/actions/{1}".format(url, 'enable_snapshot_plan' if action_enable else 'disable_snapshot_plan') - try: - api_apply_action( - module, - action_url, - action if action_enable else None, - 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 the snapshot plan: {0}'.format(exc)) + except ApplyActionError as exc: + module.fail_json(msg='Error while updating the snapshot plan: {0}'.format(exc)) - changed = action_enable is not None - before = [before] - after = [after] + changed = action_enable is not None + before = [before] + after = [after] module.exit_json( changed=changed, diff --git a/plugins/modules/storagebox_snapshot_plan_info.py b/plugins/modules/storagebox_snapshot_plan_info.py index 04e8c462..e18c8515 100644 --- a/plugins/modules/storagebox_snapshot_plan_info.py +++ b/plugins/modules/storagebox_snapshot_plan_info.py @@ -18,11 +18,11 @@ description: - Query the snapshot plans for a storage box. 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 - community.hrobot.attributes.idempotent_not_modify_state @@ -101,6 +101,8 @@ - The month of execution of the plan. V(1) is January, V(12) is December. - If set to V(null), the plan is run every month. - Always V(null) if O(hetzner_token) is provided. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: int sample: null returned: success @@ -115,10 +117,8 @@ from ansible.module_utils.basic import AnsibleModule 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 ( @@ -128,18 +128,9 @@ api_fetch_url_json, ) - -def extract_legacy(result): - sb = result['snapshotplan'] - return { - 'status': sb['status'], - 'minute': sb['minute'], - 'hour': sb['hour'], - 'day_of_week': sb['day_of_week'], - 'day_of_month': sb['day_of_month'], - 'month': sb['month'], - 'max_snapshots': sb['max_snapshots'], - } +from ansible_collections.community.hrobot.plugins.module_utils._tagging import ( + deprecate_value, +) def extract(result): @@ -152,7 +143,7 @@ def extract(result): 'hour': None, 'day_of_week': None, 'day_of_month': None, - 'month': None, + 'month': deprecate_value(None, "The return value `month` is deprecated; it is always null.", version="3.0.0"), 'max_snapshots': None, } @@ -162,7 +153,7 @@ def extract(result): 'hour': sp['hour'], 'day_of_week': sp['day_of_week'], 'day_of_month': sp['day_of_month'], - 'month': None, + 'month': deprecate_value(None, "The return value `month` is deprecated; it is always null.", version="3.0.0"), 'max_snapshots': sp['max_snapshots'], } @@ -172,7 +163,7 @@ def main(): storagebox_id=dict(type='int', required=True), ) 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( @@ -182,28 +173,22 @@ def main(): storagebox_id = module.params['storagebox_id'] + 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}/snapshotplan".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)) - - # The documentation (https://robot.hetzner.com/doc/webservice/en.html#get-storagebox-storagebox-id-snapshotplan) - # claims that the result is a list, but actually it is a dictionary. Convert it to a list of dicts if that's the case. - if isinstance(result, dict): - result = [result] - - plans = [extract_legacy(plan) for plan in 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"]) - if error: - module.fail_json(msg='Storagebox with ID {0} does not exist'.format(storagebox_id)) - - plans = [extract(result)] + 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)) + + plans = [extract(result)] module.exit_json( changed=False, diff --git a/plugins/modules/storagebox_subaccount.py b/plugins/modules/storagebox_subaccount.py index 5d30a9b1..0e1ca54b 100644 --- a/plugins/modules/storagebox_subaccount.py +++ b/plugins/modules/storagebox_subaccount.py @@ -19,11 +19,11 @@ description: - Create, update, or delete a subaccount for a storage box. 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 @@ -270,19 +270,20 @@ description: - The subaccount object returned by the API. - If O(hetzner_token) is provided, some extra fields are added to make this more compatible with the format returned by O(hetzner_user). + - B(This extra return values are deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using these return values. + These return values are RV(ignore:homedirectory), RV(ignore:samba), RV(ignore:ssh), RV(ignore:webdav), RV(ignore:external_reachability), + RV(ignore:readonly), RV(ignore:createtime), and RV(ignore:comment). type: dict returned: if O(state=present) """ from copy import deepcopy from ansible.module_utils.basic import AnsibleModule -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 ( @@ -294,126 +295,9 @@ api_fetch_url_json, ) - -def legacy_encode_data(data): - """Converts booleans to lowercase strings and filters out None values.""" - return urlencode( - { - key: str(value).lower() if isinstance(value, bool) else value - for key, value in data.items() - if value is not None - } - ) - - -def legacy_create_subaccount(module, storagebox_id, subaccount): - url = "{0}/storagebox/{1}/subaccount".format(BASE_URL, storagebox_id) - res, error = fetch_url_json( - module, - url, - method="POST", - data=legacy_encode_data(subaccount), - headers={"Content-type": "application/x-www-form-urlencoded"}, - accept_errors=[ - "STORAGEBOX_SUBACCOUNT_LIMIT_EXCEEDED", - "STORAGEBOX_INVALID_PASSWORD", - ], - timeout=30000, # this endpoint is stupidly slow - ) - - if error == "STORAGEBOX_INVALID_PASSWORD": - module.fail_json(msg="Invalid password (says Hetzner)") - if error == "STORAGEBOX_SUBACCOUNT_LIMIT_EXCEEDED": - module.fail_json(msg="Subaccount limit exceeded") - - # Contains all subaccount informations - # { "subaccount": } - return res["subaccount"] - - -def legacy_merge_subaccounts_infos(original, updates): - # None values aren't updated - result = original.copy() - for key, value in updates.items(): - if value is not None: - result[key] = value - return result - - -def legacy_is_subaccount_updated(before, after): - for key, value in after.items(): - # Means user didn't provide a value - # we assume we don't want to update that field - if value is None: - continue - # password aren't considered part of update check - # due to being a different API call - if key == "password": - continue - if before.get(key) != value: - return True - return False - - -def legacy_delete_subaccount(module, storagebox_id, subaccount): - empty, error = fetch_url_json( - module, - "{0}/storagebox/{1}/subaccount/{2}".format( - BASE_URL, storagebox_id, subaccount["username"] - ), - method="DELETE", - allow_empty_result=True, - headers={"Content-type": "application/x-www-form-urlencoded"}, - ) - - -def legacy_update_subaccount(module, storagebox_id, subaccount): - empty, error = fetch_url_json( - module, - "{0}/storagebox/{1}/subaccount/{2}".format( - BASE_URL, storagebox_id, subaccount["username"] - ), - method="PUT", - data=legacy_encode_data({key: value for key, value in subaccount.items() if key != "password"}), - headers={"Content-type": "application/x-www-form-urlencoded"}, - allow_empty_result=True, - timeout=30000, # this endpoint is stupidly slow - ) - - -def legacy_update_subaccount_password(module, storagebox_id, subaccount): - new_password, error = fetch_url_json( - module, - "{0}/storagebox/{1}/subaccount/{2}/password".format( - BASE_URL, storagebox_id, subaccount["username"] - ), - method="POST", - data=legacy_encode_data({"password": subaccount["password"]}), - headers={"Content-type": "application/x-www-form-urlencoded"}, - accept_errors=[ - "STORAGEBOX_INVALID_PASSWORD", - ], - timeout=30000, # this endpoint is stupidly slow - ) - if error == "STORAGEBOX_INVALID_PASSWORD": - module.fail_json(msg="Invalid password (says Hetzner)") - - # { "password": } - return new_password["password"] - - -def legacy_get_subaccounts(module, storagebox_id): - url = "{0}/storagebox/{1}/subaccount".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) - ) - # Hetzner's response [ { "subaccount": }, ... ] - return [item["subaccount"] for item in result] - - -# ----------------------------------------- +from ansible_collections.community.hrobot.plugins.module_utils._tagging import ( + deprecate_value, +) def create_subaccount(module, storagebox_id, subaccount): @@ -607,7 +491,11 @@ def adjust_legacy(subaccount): }.items(): value, exists = get_value_opt(subaccount, path) if exists: - result[key] = value + result[key] = deprecate_value( + value, + "The return value `{0}` is deprecated; use `{1}` instead.".format(key, ".".join(path)), + version="3.0.0", + ) return result @@ -633,7 +521,7 @@ def main(): idempotence=dict(type="str", choices=["username", "comment"], default="username"), ) 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( @@ -659,140 +547,84 @@ def main(): } account_identifier = subaccount[idempotence] + 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 - - existing_subaccounts = legacy_get_subaccounts(module, storagebox_id) - - matches = [ - sa for sa in existing_subaccounts - if sa[idempotence] == account_identifier - ] - if len(matches) > 1: - module.fail_json(msg="More than one subaccount matched the idempotence criteria.") - - existing = matches[0] if matches else None - - created = deleted = updated = password_updated = False - - if state == "absent": - if existing: - if not check_mode: - legacy_delete_subaccount(module, storagebox_id, existing) - deleted = True - elif state == "present" and existing: - # Set the found username in case user used comment as idempotence - subaccount["username"] = existing["username"] - - if ( - password_mode == "set-to-random" or - (password_mode == "update-if-provided" and subaccount["password"]) - ): - if password_mode == "set-to-random": - subaccount["password"] = None - if not check_mode: - new_password = legacy_update_subaccount_password(module, storagebox_id, subaccount) - subaccount["password"] = new_password - password_updated = True - - if legacy_is_subaccount_updated(existing, subaccount): - if not check_mode: - legacy_update_subaccount(module, storagebox_id, subaccount) - updated = True - else: # state 'present' without pre-existing account - if not subaccount["homedirectory"]: - module.fail_json(msg="homedirectory is required when creating a new subaccount") - if password_mode == "set-to-random": - subaccount["password"] = None - - del subaccount["username"] # username cannot be choosen + 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)) + + if password_mode == 'set-to-random': + module.fail_json(msg="The new Hetzner API does not support password_mode=set-to-random") + if idempotence == 'comment': + idempotence = 'description' + + existing_subaccounts = get_subaccounts(module, storagebox_id) + + matches = [ + sa for sa in existing_subaccounts + if sa[idempotence] == account_identifier + ] + if len(matches) > 1: + module.fail_json(msg="More than one subaccount matched the idempotence criteria.") + + existing = matches[0] if matches else None + + created = deleted = updated = password_updated = False + + if state == "absent": + if existing: if not check_mode: - # not necessary, allows us to get additional infos (created time etc...) - existing = legacy_create_subaccount(module, storagebox_id, subaccount) - created = True - - return_data = legacy_merge_subaccounts_infos(existing or {}, subaccount) - - module.exit_json( - changed=any([created, deleted, updated, password_updated]), - created=created, - deleted=deleted, - updated=updated, - password_updated=password_updated, - subaccount=return_data if state != "absent" else None, - ) + delete_subaccount(module, storagebox_id, existing) + deleted = True + elif state == "present" and existing: + # Set the found username in case user used comment as idempotence + subaccount["username"] = existing["username"] + + if ( + password_mode == "update-if-provided" and subaccount["password"] + ): + if not check_mode: + update_subaccount_password(module, storagebox_id, existing, subaccount["password"]) + password_updated = True - else: - # NEW API! - - if password_mode == 'set-to-random': - module.fail_json(msg="The new Hetzner API does not support password_mode=set-to-random") - if idempotence == 'comment': - idempotence = 'description' - - existing_subaccounts = get_subaccounts(module, storagebox_id) - - matches = [ - sa for sa in existing_subaccounts - if sa[idempotence] == account_identifier - ] - if len(matches) > 1: - module.fail_json(msg="More than one subaccount matched the idempotence criteria.") - - existing = matches[0] if matches else None - - created = deleted = updated = password_updated = False - - if state == "absent": - if existing: - if not check_mode: - delete_subaccount(module, storagebox_id, existing) - deleted = True - elif state == "present" and existing: - # Set the found username in case user used comment as idempotence - subaccount["username"] = existing["username"] - - if ( - password_mode == "update-if-provided" and subaccount["password"] - ): - if not check_mode: - update_subaccount_password(module, storagebox_id, existing, subaccount["password"]) - password_updated = True - - update, access_settings = get_subaccount_updates(existing, subaccount) - if update: - if not check_mode: - update_subaccount(module, storagebox_id, existing, update) - updated = True - if access_settings: - if not check_mode: - update_access_settings(module, storagebox_id, existing, access_settings) - updated = True - else: # state 'present' without pre-existing account - if not subaccount["homedirectory"]: - module.fail_json(msg="homedirectory is required when creating a new subaccount") - if not subaccount["password"]: - module.fail_json(msg="password is required when creating a new subaccount") - - del subaccount["username"] # username cannot be choosen + update, access_settings = get_subaccount_updates(existing, subaccount) + if update: if not check_mode: - new_subaccount_id = create_subaccount(module, storagebox_id, subaccount) - # Retrieve created subaccount - # (not necessary, allows us to get additional infos (created time etc...)) - url = "{0}/v1/storage_boxes/{1}/subaccounts/{2}".format(API_BASE_URL, storagebox_id, new_subaccount_id) - existing = api_fetch_url_json(module, url, method='GET')[0]["subaccount"] - created = True - - return_data = merge_subaccounts_infos(existing or {}, subaccount) - - module.exit_json( - changed=any([created, deleted, updated, password_updated]), - created=created, - deleted=deleted, - updated=updated, - password_updated=password_updated, - subaccount=adjust_legacy(return_data) if state != "absent" else None, - ) + update_subaccount(module, storagebox_id, existing, update) + updated = True + if access_settings: + if not check_mode: + update_access_settings(module, storagebox_id, existing, access_settings) + updated = True + else: # state 'present' without pre-existing account + if not subaccount["homedirectory"]: + module.fail_json(msg="homedirectory is required when creating a new subaccount") + if not subaccount["password"]: + module.fail_json(msg="password is required when creating a new subaccount") + + del subaccount["username"] # username cannot be choosen + if not check_mode: + new_subaccount_id = create_subaccount(module, storagebox_id, subaccount) + # Retrieve created subaccount + # (not necessary, allows us to get additional infos (created time etc...)) + url = "{0}/v1/storage_boxes/{1}/subaccounts/{2}".format(API_BASE_URL, storagebox_id, new_subaccount_id) + existing = api_fetch_url_json(module, url, method='GET')[0]["subaccount"] + created = True + + return_data = merge_subaccounts_infos(existing or {}, subaccount) + + module.exit_json( + changed=any([created, deleted, updated, password_updated]), + created=created, + deleted=deleted, + updated=updated, + password_updated=password_updated, + subaccount=adjust_legacy(return_data) if state != "absent" else None, + ) if __name__ == "__main__": # pragma: no cover diff --git a/plugins/modules/storagebox_subaccount_info.py b/plugins/modules/storagebox_subaccount_info.py index f3d3f9ea..caba8df0 100644 --- a/plugins/modules/storagebox_subaccount_info.py +++ b/plugins/modules/storagebox_subaccount_info.py @@ -19,11 +19,11 @@ description: - Query the subaccounts for a storage box. 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 - community.hrobot.attributes.idempotent_not_modify_state @@ -72,9 +72,11 @@ description: - Username of the main user. - Not supported by the new Hetzner API. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: str sample: "u2342" - returned: success if O(hetzner_token) is not specified + returned: success and if O(hetzner_token) is not specified server: description: - Server on which the sub-account resides. @@ -85,6 +87,8 @@ description: - Homedirectory of the sub-account. - Note that this is copied from RV(subaccounts[].home_directory) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: str sample: "/home/u2342-sub1" returned: success @@ -92,6 +96,8 @@ description: - Status of Samba support. - Note that this is copied from RV(subaccounts[].access_settings.samba_enabled) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: true returned: success @@ -99,6 +105,8 @@ description: - Status of SSH support. - Note that this is copied from RV(subaccounts[].access_settings.ssh_enabled) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: true returned: success @@ -106,6 +114,8 @@ description: - Status of external reachability. - Note that this is copied from RV(subaccounts[].access_settings.reachable_externally) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: false returned: success @@ -113,6 +123,8 @@ description: - Status of WebDAV support. - Note that this is copied from RV(subaccounts[].access_settings.webdav_enabled) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: true returned: success @@ -120,6 +132,8 @@ description: - Indicates if the sub-account is in readonly mode. - Note that this is copied from RV(subaccounts[].access_settings.readonly) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: bool sample: false returned: success @@ -127,6 +141,8 @@ description: - Timestamp when the sub-account was created. - Note that this is copied from RV(subaccounts[].created) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: str sample: "2023-08-25T14:23:05Z" returned: success @@ -134,6 +150,8 @@ description: - Custom comment for the sub-account. - Note that this is copied from RV(subaccounts[].description) in case O(hetzner_token) is specified. + - B(This return value is deprecated and will be removed from community.hrobot 3.0.0.) + If you are using ansible-core 2.19 or newer, you will see a deprecation message when using this return value. type: str sample: "This is a subaccount" returned: success @@ -208,10 +226,8 @@ from ansible.module_utils.basic import AnsibleModule 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 ( @@ -221,17 +237,53 @@ api_fetch_url_json, ) +from ansible_collections.community.hrobot.plugins.module_utils._tagging import ( + deprecate_value, +) + def adjust_legacy(subaccount): result = dict(subaccount) - result['homedirectory'] = subaccount['home_directory'] - result['samba'] = subaccount['access_settings']['samba_enabled'] - result['ssh'] = subaccount['access_settings']['ssh_enabled'] - result['webdav'] = subaccount['access_settings']['webdav_enabled'] - result['external_reachability'] = subaccount['access_settings']['reachable_externally'] - result['readonly'] = subaccount['access_settings']['readonly'] - result['createtime'] = subaccount['created'] - result['comment'] = subaccount['description'] + result['homedirectory'] = deprecate_value( + subaccount['home_directory'], + "The return value `homedirectory` is deprecated; use `home_directory` instead.", + version="3.0.0", + ) + result['samba'] = deprecate_value( + subaccount['access_settings']['samba_enabled'], + "The return value `samba` is deprecated; use `access_settings.samba_enabled` instead.", + version="3.0.0", + ) + result['ssh'] = deprecate_value( + subaccount['access_settings']['ssh_enabled'], + "The return value `ssh` is deprecated; use `access_settings.ssh_enabled` instead.", + version="3.0.0", + ) + result['webdav'] = deprecate_value( + subaccount['access_settings']['webdav_enabled'], + "The return value `webdav` is deprecated; use `access_settings.webdav_enabled` instead.", + version="3.0.0", + ) + result['external_reachability'] = deprecate_value( + subaccount['access_settings']['reachable_externally'], + "The return value `external_reachability` is deprecated; use `access_settings.reachable_externally` instead.", + version="3.0.0", + ) + result['readonly'] = deprecate_value( + subaccount['access_settings']['readonly'], + "The return value `readonly` is deprecated; use `access_settings.readonly` instead.", + version="3.0.0", + ) + result['createtime'] = deprecate_value( + subaccount['created'], + "The return value `createtime` is deprecated; use `created` instead.", + version="3.0.0", + ) + result['comment'] = deprecate_value( + subaccount['description'], + "The return value `comment` is deprecated; use `description` instead.", + version="3.0.0", + ) return result @@ -240,7 +292,7 @@ def main(): storagebox_id=dict(type="int", required=True), ) 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( @@ -250,33 +302,25 @@ def main(): storagebox_id = module.params["storagebox_id"] - if module.params["hetzner_user"] is not None: - # DEPRECATED: old API - - url = "{0}/storagebox/{1}/subaccount".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) - ) - - module.exit_json( - changed=False, - subaccounts=[item["subaccount"] for item in result], + 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: + 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)) - else: - # NEW API! - - url = "{0}/v1/storage_boxes/{1}/subaccounts".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)) + url = "{0}/v1/storage_boxes/{1}/subaccounts".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)) - module.exit_json( - changed=False, - subaccounts=[adjust_legacy(item) for item in result['subaccounts']], - ) + module.exit_json( + changed=False, + subaccounts=[adjust_legacy(item) for item in result['subaccounts']], + ) if __name__ == "__main__": # pragma: no cover diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 7a023ea7..4282c63b 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -1,3 +1,2 @@ plugins/inventory/robot.py yamllint:unparsable-with-libyaml -plugins/modules/storagebox_snapshot_plan.py pylint:unbalanced-dict-unpacking # only seems to be a problem with pylint included in ansible-core 2.15 tests/ee/roles/smoke/library/smoke_ipaddress.py shebang diff --git a/tests/unit/plugins/modules/test_server_info.py b/tests/unit/plugins/modules/test_server_info.py index 9a9c715d..98774e9f 100644 --- a/tests/unit/plugins/modules/test_server_info.py +++ b/tests/unit/plugins/modules/test_server_info.py @@ -11,6 +11,8 @@ BaseTestModule, ) +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import call, MagicMock + from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import server_info @@ -295,3 +297,172 @@ def test_server_name_none_error(self, mocker): ]) assert result['changed'] is False assert len(result['servers']) == 0 + + def test_server_number_rate_limit_fail(self, mocker): + result = self.run_module_failed(mocker, server_info, { + 'hetzner_user': 'test', + 'hetzner_password': 'hunter2', + 'server_number': 23, + 'rate_limit_retry_timeout': 0, + }, [ + FetchUrlCall('GET', 403) + .expect_basic_auth('test', 'hunter2') + .expect_force_basic_auth(True) + .expect_url('{0}/server/23'.format(BASE_URL)) + .result_json({ + 'error': { + 'status': 403, + 'code': 'RATE_LIMIT_EXCEEDED', + 'message': 'Rate limit exceeded', + 'interval': 5, + 'max_request': 1, + }, + }), + ]) + assert result['msg'] == ( + 'Request failed: 403 RATE_LIMIT_EXCEEDED (Rate limit exceeded).' + ' Maximum allowed requests: 1. Time interval in seconds: 5' + ) + + def test_server_number_rate_limit(self, mocker): + sleep_mock = MagicMock() + mocker.patch('time.sleep', sleep_mock) + result = self.run_module_success(mocker, server_info, { + 'hetzner_user': 'test', + 'hetzner_password': 'hunter2', + 'server_number': 23, + 'rate_limit_retry_timeout': -1, + }, [ + FetchUrlCall('GET', 403) + .expect_basic_auth('test', 'hunter2') + .expect_force_basic_auth(True) + .expect_url('{0}/server/23'.format(BASE_URL)) + .result_json({ + 'error': { + 'status': 403, + 'code': 'RATE_LIMIT_EXCEEDED', + 'message': 'Rate limit exceeded', + 'interval': 5, + 'max_request': 1, + }, + }), + FetchUrlCall('GET', 403) + .expect_basic_auth('test', 'hunter2') + .expect_force_basic_auth(True) + .expect_url('{0}/server/23'.format(BASE_URL)) + .result_json({ + 'error': { + 'status': 403, + 'code': 'RATE_LIMIT_EXCEEDED', + 'message': 'Rate limit exceeded', + 'interval': 3, + 'max_request': 1, + }, + }), + FetchUrlCall('GET', 403) + .expect_basic_auth('test', 'hunter2') + .expect_force_basic_auth(True) + .expect_url('{0}/server/23'.format(BASE_URL)) + .result_json({ + 'error': { + 'status': 403, + 'code': 'RATE_LIMIT_EXCEEDED', + 'message': 'Rate limit exceeded', + 'interval': 4, + 'max_request': 1, + }, + }), + FetchUrlCall('GET', 403) + .expect_basic_auth('test', 'hunter2') + .expect_force_basic_auth(True) + .expect_url('{0}/server/23'.format(BASE_URL)) + .result_json({ + 'error': { + 'status': 403, + 'code': 'RATE_LIMIT_EXCEEDED', + 'message': 'Rate limit exceeded', + 'interval': 5, + 'max_request': 1, + }, + }), + FetchUrlCall('GET', 200) + .expect_basic_auth('test', 'hunter2') + .expect_force_basic_auth(True) + .result_json(SERVER_DETAIL_DATA[23]) + .expect_url('{0}/server/23'.format(BASE_URL)), + ]) + assert result['changed'] is False + assert len(result['servers']) == 1 + assert result['servers'][0] == SERVER_DETAIL_DATA[23]['server'] + sleep_mock.assert_has_calls([ + call(5), + call(3), + call(3), + call(3), + ]) + + def test_server_number_rate_limit_timeout(self, mocker): + elapsed = [123.4] + + def sleep(duration): + elapsed[0] += duration + print('sleep', duration, '->', elapsed[0]) + + def get_time(): + elapsed[0] += 0.03 + print('get', elapsed[0]) + return elapsed[0] + + mocker.patch('time.sleep', sleep) + mocker.patch('time.time', get_time) + result = self.run_module_failed(mocker, server_info, { + 'hetzner_user': 'test', + 'hetzner_password': 'hunter2', + 'server_number': 23, + 'rate_limit_retry_timeout': 7, + }, [ + FetchUrlCall('GET', 403) + .expect_basic_auth('test', 'hunter2') + .expect_force_basic_auth(True) + .expect_url('{0}/server/23'.format(BASE_URL)) + .result_json({ + 'error': { + 'status': 403, + 'code': 'RATE_LIMIT_EXCEEDED', + 'message': 'Rate limit exceeded', + 'interval': 5, + 'max_request': 1, + }, + }), + FetchUrlCall('GET', 403) + .expect_basic_auth('test', 'hunter2') + .expect_force_basic_auth(True) + .expect_url('{0}/server/23'.format(BASE_URL)) + .result_json({ + 'error': { + 'status': 403, + 'code': 'RATE_LIMIT_EXCEEDED', + 'message': 'Rate limit exceeded', + 'interval': 3, + 'max_request': 1, + }, + }), + FetchUrlCall('GET', 403) + .expect_basic_auth('test', 'hunter2') + .expect_force_basic_auth(True) + .expect_url('{0}/server/23'.format(BASE_URL)) + .result_json({ + 'error': { + 'status': 403, + 'code': 'RATE_LIMIT_EXCEEDED', + 'message': 'Rate limit exceeded', + 'interval': 4, + 'max_request': 1, + }, + }), + ]) + assert result['msg'] == ( + 'Request failed: 403 RATE_LIMIT_EXCEEDED (Rate limit exceeded).' + ' Maximum allowed requests: 1. Time interval in seconds: 4.' + ' Waited a total of 5.1 seconds for rate limit errors to go away' + ) diff --git a/tests/unit/plugins/modules/test_storagebox.py b/tests/unit/plugins/modules/test_storagebox.py index 18e0693e..aa1007e4 100644 --- a/tests/unit/plugins/modules/test_storagebox.py +++ b/tests/unit/plugins/modules/test_storagebox.py @@ -14,63 +14,9 @@ from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import call, MagicMock from ansible_collections.community.hrobot.plugins.module_utils.api import API_BASE_URL -from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import storagebox -STORAGEBOX_LEGACY_KEYS = ('name', 'webdav', 'samba', 'ssh', 'external_reachability', 'zfs') - -STORAGEBOX_LEGACY_DETAIL_DATA = { - 23: { - 'storagebox': { - 'id': 23, - 'login': 'u23', - 'name': 'Backup Server 2', - 'product': 'BX11', - 'cancelled': True, - 'locked': False, - 'location': 'HEL1', - 'linked_server': None, - 'paid_until': '2025-01-31', - 'disk_quota': 1234, - 'disk_usage': 123, - 'disk_usage_data': 50, - 'disk_usage_snapshots': 73, - 'webdav': False, - 'samba': False, - 'ssh': True, - 'external_reachability': True, - 'zfs': False, - 'server': 'u23.your-storagebox.de', - 'host_system': 'HEL1-FOOBAR' - }, - }, - 123456: { - 'storagebox': { - 'id': 123456, - 'login': 'u12345', - 'name': 'Backup Server 1', - 'product': 'BX60', - 'cancelled': False, - 'locked': False, - 'location': 'FSN1', - 'linked_server': 1234567, - 'paid_until': '2015-10-23', - 'disk_quota': 10240000, - 'disk_usage': 900, - 'disk_usage_data': 500, - 'disk_usage_snapshots': 400, - 'webdav': True, - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'zfs': False, - 'server': 'u12345.your-storagebox.de', - 'host_system': 'FSN1-BX355' - }, - }, -} - STORAGEBOX_KEYS = { 'name': ['name'], 'webdav': ['access_settings', 'webdav_enabled'], @@ -231,14 +177,6 @@ } -def legacy_update_info(id, **updates): - result = { - 'storagebox': dict(STORAGEBOX_LEGACY_DETAIL_DATA[id]['storagebox']), - } - result['storagebox'].update(updates) - return result - - def update_info(id, **updates): result = { 'storage_box': dict(STORAGEBOX_DETAIL_DATA[id]), @@ -263,363 +201,15 @@ class TestHetznerStorageboxLegacy(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox.AnsibleModule' MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.hrobot.plugins.module_utils.robot.fetch_url' - def test_idempotent(self, mocker): - updated = legacy_update_info(23) - result = self.run_module_success(mocker, storagebox, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'id': 23, - 'name': 'Backup Server 2', - 'webdav': False, - 'samba': False, - 'ssh': True, - 'external_reachability': True, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(STORAGEBOX_LEGACY_DETAIL_DATA[23]) - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - ]) - assert result['changed'] is False - for key in STORAGEBOX_LEGACY_KEYS: - assert result[key] == updated['storagebox'][key], "Unexpected difference for {0!r}".format(key) - def test_id_unknown(self, mocker): result = self.run_module_failed(mocker, storagebox, { 'hetzner_user': '', 'hetzner_password': '', 'id': 1, }, [ - FetchUrlCall('GET', 404) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'Storagebox not found', - }, - }) - .expect_url('{0}/storagebox/1'.format(BASE_URL)), ]) assert result['msg'] == 'Storagebox with ID 1 does not exist' - def test_invalid_input(self, mocker): - result = self.run_module_failed(mocker, storagebox, { - 'hetzner_user': '', - 'hetzner_password': '', - 'id': 23, - 'name': 'Backup', - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json(STORAGEBOX_LEGACY_DETAIL_DATA[23]) - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 400) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json({ - 'error': { - 'status': 400, - 'code': 'INVALID_INPUT', - 'message': 'Invalid input', - 'invalid': ['storagebox_name'], - 'missing': None, - }, - }) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - ]) - assert result['msg'] == 'The values to update were invalid (storagebox_name)' - - def test_change_name(self, mocker): - updated = legacy_update_info(23, name='Backup') - result = self.run_module_success(mocker, storagebox, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'id': 23, - 'name': 'Backup', - 'ssh': True, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(STORAGEBOX_LEGACY_DETAIL_DATA[23]) - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 200) - .result_json(updated) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - ]) - assert result['changed'] is True - for key in STORAGEBOX_LEGACY_KEYS: - assert result[key] == updated['storagebox'][key], "Unexpected difference for {0!r}".format(key) - - def test_change_name_rate_limit_fail(self, mocker): - result = self.run_module_failed(mocker, storagebox, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'id': 23, - 'name': 'Backup', - 'ssh': True, - 'rate_limit_retry_timeout': 0, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(STORAGEBOX_LEGACY_DETAIL_DATA[23]) - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 403) - .result_json({ - 'error': { - 'status': 403, - 'code': 'RATE_LIMIT_EXCEEDED', - 'message': 'Rate limit exceeded', - 'interval': 5, - 'max_request': 1, - }, - }) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - ]) - assert result['msg'] == ( - 'Request failed: 403 RATE_LIMIT_EXCEEDED (Rate limit exceeded).' - ' Maximum allowed requests: 1. Time interval in seconds: 5' - ) - - def test_change_name_rate_limit(self, mocker): - sleep_mock = MagicMock() - mocker.patch('time.sleep', sleep_mock) - updated = legacy_update_info(23, name='Backup') - result = self.run_module_success(mocker, storagebox, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'id': 23, - 'name': 'Backup', - 'ssh': True, - 'rate_limit_retry_timeout': -1, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(STORAGEBOX_LEGACY_DETAIL_DATA[23]) - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 403) - .result_json({ - 'error': { - 'status': 403, - 'code': 'RATE_LIMIT_EXCEEDED', - 'message': 'Rate limit exceeded', - 'interval': 5, - 'max_request': 1, - }, - }) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 403) - .result_json({ - 'error': { - 'status': 403, - 'code': 'RATE_LIMIT_EXCEEDED', - 'message': 'Rate limit exceeded', - 'interval': 3, - 'max_request': 1, - }, - }) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 403) - .result_json({ - 'error': { - 'status': 403, - 'code': 'RATE_LIMIT_EXCEEDED', - 'message': 'Rate limit exceeded', - 'interval': 4, - 'max_request': 1, - }, - }) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 403) - .result_json({ - 'error': { - 'status': 403, - 'code': 'RATE_LIMIT_EXCEEDED', - 'message': 'Rate limit exceeded', - }, - }) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 200) - .result_json(updated) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - ]) - assert result['changed'] is True - for key in STORAGEBOX_LEGACY_KEYS: - assert result[key] == updated['storagebox'][key], "Unexpected difference for {0!r}".format(key) - sleep_mock.assert_has_calls([ - call(5), - call(3), - call(3), - call(3), - ]) - - def test_change_name_rate_limit_timeout(self, mocker): - elapsed = [123.4] - - def sleep(duration): - elapsed[0] += duration - print('sleep', duration, '->', elapsed[0]) - - def get_time(): - elapsed[0] += 0.03 - print('get', elapsed[0]) - return elapsed[0] - - mocker.patch('time.sleep', sleep) - mocker.patch('time.time', get_time) - result = self.run_module_failed(mocker, storagebox, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'id': 23, - 'name': 'Backup', - 'ssh': True, - 'rate_limit_retry_timeout': 7, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(STORAGEBOX_LEGACY_DETAIL_DATA[23]) - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 403) - .result_json({ - 'error': { - 'status': 403, - 'code': 'RATE_LIMIT_EXCEEDED', - 'message': 'Rate limit exceeded', - 'interval': 5, - 'max_request': 1, - }, - }) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 403) - .result_json({ - 'error': { - 'status': 403, - 'code': 'RATE_LIMIT_EXCEEDED', - 'message': 'Rate limit exceeded', - 'interval': 5, - 'max_request': 1, - }, - }) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 403) - .result_json({ - 'error': { - 'status': 403, - 'code': 'RATE_LIMIT_EXCEEDED', - 'message': 'Rate limit exceeded', - 'interval': 5, - 'max_request': 1, - }, - }) - .expect_form_value('storagebox_name', 'Backup') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value_absent('ssh') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - ]) - assert result['msg'] == ( - 'Request failed: 403 RATE_LIMIT_EXCEEDED (Rate limit exceeded).' - ' Maximum allowed requests: 1. Time interval in seconds: 5.' - ' Waited a total of 5.1 seconds for rate limit errors to go away' - ) - - def test_change_ssh(self, mocker): - updated = legacy_update_info(23, ssh=False) - result = self.run_module_success(mocker, storagebox, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'id': 23, - 'name': 'Backup Server 2', - 'ssh': False, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(STORAGEBOX_LEGACY_DETAIL_DATA[23]) - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - FetchUrlCall('POST', 200) - .result_json(updated) - .expect_form_value_absent('storagebox_name') - .expect_form_value_absent('webdav') - .expect_form_value_absent('samba') - .expect_form_value('ssh', 'false') - .expect_form_value_absent('external_reachability') - .expect_form_value_absent('zfs') - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - ]) - assert result['changed'] is True - for key in STORAGEBOX_LEGACY_KEYS: - assert result[key] == updated['storagebox'][key], "Unexpected difference for {0!r}".format(key) - class TestHetznerStoragebox(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox.AnsibleModule' diff --git a/tests/unit/plugins/modules/test_storagebox_info.py b/tests/unit/plugins/modules/test_storagebox_info.py index 0b308e90..e75f0e31 100644 --- a/tests/unit/plugins/modules/test_storagebox_info.py +++ b/tests/unit/plugins/modules/test_storagebox_info.py @@ -14,91 +14,9 @@ from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import call, MagicMock from ansible_collections.community.hrobot.plugins.module_utils.api import API_BASE_URL -from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import storagebox_info -STORAGEBOX_MINIMUM_DATA = [ - { - 'storagebox': { - 'id': 123456, - 'login': 'u12345', - 'name': 'Backup Server 1', - 'product': 'BX60', - 'cancelled': False, - 'locked': False, - 'location': 'FSN1', - 'linked_server': 1234567, - 'paid_until': '2015-10-23', - }, - }, - { - 'storagebox': { - 'id': 23, - 'login': 'u23', - 'name': 'Backup Server 2', - 'product': 'BX11', - 'cancelled': True, - 'locked': False, - 'location': 'HEL1', - 'linked_server': None, - 'paid_until': '2025-01-31', - }, - }, -] - - -STORAGEBOX_DETAIL_DATA = { - 23: { - 'storagebox': { - 'id': 23, - 'login': 'u23', - 'name': 'Backup Server 2', - 'product': 'BX11', - 'cancelled': True, - 'locked': False, - 'location': 'HEL1', - 'linked_server': None, - 'paid_until': '2025-01-31', - 'disk_quota': 1234, - 'disk_usage': 123, - 'disk_usage_data': 50, - 'disk_usage_snapshots': 73, - 'webdav': False, - 'samba': False, - 'ssh': True, - 'external_reachability': True, - 'zfs': False, - 'server': 'u23.your-storagebox.de', - 'host_system': 'HEL1-FOOBAR' - }, - }, - 123456: { - 'storagebox': { - 'id': 123456, - 'login': 'u12345', - 'name': 'Backup Server 1', - 'product': 'BX60', - 'cancelled': False, - 'locked': False, - 'location': 'FSN1', - 'linked_server': 1234567, - 'paid_until': '2015-10-23', - 'disk_quota': 10240000, - 'disk_usage': 900, - 'disk_usage_data': 500, - 'disk_usage_snapshots': 400, - 'webdav': True, - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'zfs': False, - 'server': 'u12345.your-storagebox.de', - 'host_system': 'FSN1-BX355' - }, - }, -} - STORAGEBOX_API_DATA = { 23: { 'id': 23, @@ -254,138 +172,12 @@ class TestHetznerStorageboxInfoLegacy(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox_info.AnsibleModule' MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.hrobot.plugins.module_utils.robot.fetch_url' - def test_storagebox_id(self, mocker): - result = self.run_module_success(mocker, storagebox_info, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(STORAGEBOX_DETAIL_DATA[23]) - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['storageboxes']) == 1 - assert result['storageboxes'][0] == STORAGEBOX_DETAIL_DATA[23]['storagebox'] - def test_server_number_unknown(self, mocker): result = self.run_module_success(mocker, storagebox_info, { 'hetzner_user': '', 'hetzner_password': '', 'storagebox_id': 1, }, [ - FetchUrlCall('GET', 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'server not found', - }, - }) - .expect_url('{0}/storagebox/1'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['storageboxes']) == 0 - - def test_all(self, mocker): - result = self.run_module_success(mocker, storagebox_info, { - 'hetzner_user': '', - 'hetzner_password': '', - }, [ - FetchUrlCall('GET', 200) - .result_json(STORAGEBOX_MINIMUM_DATA) - .expect_url('{0}/storagebox'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['storageboxes']) == 2 - assert result['storageboxes'][0] == STORAGEBOX_MINIMUM_DATA[0]['storagebox'] - assert result['storageboxes'][1] == STORAGEBOX_MINIMUM_DATA[1]['storagebox'] - - def test_linked_server_number(self, mocker): - result = self.run_module_success(mocker, storagebox_info, { - 'hetzner_user': '', - 'hetzner_password': '', - 'linked_server_number': 1234567, - }, [ - FetchUrlCall('GET', 200) - .result_json(STORAGEBOX_MINIMUM_DATA[0]) - .expect_form_value('linked_server', '1234567') - .expect_url('{0}/storagebox'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['storageboxes']) == 1 - assert result['storageboxes'][0] == STORAGEBOX_MINIMUM_DATA[0]['storagebox'] - - def test_linked_server_number_unknown(self, mocker): - result = self.run_module_success(mocker, storagebox_info, { - 'hetzner_user': '', - 'hetzner_password': '', - 'linked_server_number': 1, - }, [ - FetchUrlCall('GET', 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'server not found', - }, - }) - .expect_form_value('linked_server', '1') - .expect_url('{0}/storagebox'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['storageboxes']) == 0 - - def test_all_full_info(self, mocker): - result = self.run_module_success(mocker, storagebox_info, { - 'hetzner_user': '', - 'hetzner_password': '', - 'full_info': True, - }, [ - FetchUrlCall('GET', 200) - .result_json(STORAGEBOX_MINIMUM_DATA) - .expect_url('{0}/storagebox'.format(BASE_URL)), - FetchUrlCall('GET', 200) - .result_json(STORAGEBOX_DETAIL_DATA[123456]) - .expect_url('{0}/storagebox/123456'.format(BASE_URL)), - FetchUrlCall('GET', 200) - .result_json(STORAGEBOX_DETAIL_DATA[23]) - .expect_url('{0}/storagebox/23'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['storageboxes']) == 2 - assert result['storageboxes'][0] == STORAGEBOX_DETAIL_DATA[123456]['storagebox'] - assert result['storageboxes'][1] == STORAGEBOX_DETAIL_DATA[23]['storagebox'] - - def test_all_none(self, mocker): - result = self.run_module_success(mocker, storagebox_info, { - 'hetzner_user': '', - 'hetzner_password': '', - }, [ - FetchUrlCall('GET', 200) - .result_json([]) - .expect_url('{0}/storagebox'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['storageboxes']) == 0 - - def test_all_none_error(self, mocker): - # According to the API docs, when no storagebox is found this API can return 404. - result = self.run_module_success(mocker, storagebox_info, { - 'hetzner_user': '', - 'hetzner_password': '', - }, [ - FetchUrlCall('GET', 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'server not found', - }, - }) - .expect_url('{0}/storagebox'.format(BASE_URL)), ]) assert result['changed'] is False assert len(result['storageboxes']) == 0 diff --git a/tests/unit/plugins/modules/test_storagebox_set_password.py b/tests/unit/plugins/modules/test_storagebox_set_password.py index 27871cf8..069bc346 100644 --- a/tests/unit/plugins/modules/test_storagebox_set_password.py +++ b/tests/unit/plugins/modules/test_storagebox_set_password.py @@ -14,7 +14,6 @@ from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock from ansible_collections.community.hrobot.plugins.module_utils.api import API_BASE_URL -from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import storagebox_set_password RANDOM_PASSWORD = 'randompassword' @@ -25,78 +24,14 @@ class TestStorageboxSetPasswordLegacy(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox_set_password.AnsibleModule' MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.hrobot.plugins.module_utils.robot.fetch_url' - def test_specific_password(self, mocker): - result = self.run_module_success(mocker, storagebox_set_password, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'id': 123, - 'password': 'newpassword' - }, [ - FetchUrlCall("POST", 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .expect_form_value('password', 'newpassword') - .result_json({'password': 'newpassword'}) - .expect_url(BASE_URL + '/storagebox/123/password'), - ]) - assert result['changed'] is True - assert result['password'] == 'newpassword' - - def test_random_password(self, mocker): - result = self.run_module_success(mocker, storagebox_set_password, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'id': 123, - }, [ - FetchUrlCall("POST", 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .expect_form_value_absent('password') - .result_json({'password': RANDOM_PASSWORD}) - .expect_url(BASE_URL + '/storagebox/123/password'), - ]) - assert result['changed'] is True - assert result['password'] == RANDOM_PASSWORD - def test_id_unknown(self, mocker): result = self.run_module_failed(mocker, storagebox_set_password, { 'hetzner_user': 'test', 'hetzner_password': 'hunter2', 'id': 456, - }, [FetchUrlCall("POST", 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'Storage Box with ID 456 not found', - }, - }) - .expect_url(BASE_URL + '/storagebox/456/password'), - ]) + }, []) assert result['msg'] == 'Storage Box with ID 456 not found' - def test_password_invalid(self, mocker): - result = self.run_module_failed(mocker, storagebox_set_password, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'id': 123, - 'password': 'invalidpassword', - }, [ - FetchUrlCall("POST", 409) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json({ - 'error': { - 'status': 409, - 'code': 'STORAGEBOX_INVALID_PASSWORD', - 'message': "The chosen password has been considered insecure or does not comply with Hetzner's password guideline", - }}) - .expect_url(BASE_URL + '/storagebox/123/password') - ]) - assert result['msg'] == "The chosen password has been considered insecure or does not comply with Hetzner's password guideline" - class TestStorageboxSetPassword(BaseTestModule): diff --git a/tests/unit/plugins/modules/test_storagebox_snapshot.py b/tests/unit/plugins/modules/test_storagebox_snapshot.py index 1f3aefd5..008485d6 100644 --- a/tests/unit/plugins/modules/test_storagebox_snapshot.py +++ b/tests/unit/plugins/modules/test_storagebox_snapshot.py @@ -14,41 +14,8 @@ from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock from ansible_collections.community.hrobot.plugins.module_utils.api import API_BASE_URL -from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import storagebox_snapshot -LEGACY_CREATED_SNAPSHOT = { - 'snapshot': { - 'name': '2025-03-28T15-20-51', - 'timestamp': '2025-03-28T16:20:51+01:00', - 'size': 0 - } -} - -LEGACY_EXISTING_SNAPSHOTS = [ - { - 'snapshot': { - 'name': '2015-12-21T12-40-38', - 'timestamp': '2015-12-21T13:40:38+00:00', - 'size': 400, - 'filesystem_size': 12345, - 'automatic': False, - 'comment': 'Test-Snapshot 1' - } - }, - { - 'snapshot': { - 'name': '2025-03-28T15-20-51', - 'timestamp': '2025-03-28T15:19:30+00:00', - 'size': 10000, - 'filesystem_size': 22345, - 'automatic': False, - 'comment': 'Test-Snapshot 2' - } - } -] - - STORAGEBOX_SNAPSHOTS = { 1: { "id": 1, @@ -83,98 +50,6 @@ class TestHetznerStorageboxSnapshotPlanInfoLegacy(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox_snapshot.AnsibleModule' MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.hrobot.plugins.module_utils.robot.fetch_url' - def test_create_snapshot(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23}, [ - FetchUrlCall('POST', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(LEGACY_CREATED_SNAPSHOT) - .expect_url(BASE_URL + '/storagebox/23/snapshot') - ]) - assert result['changed'] is True - assert result['snapshot'] == LEGACY_CREATED_SNAPSHOT['snapshot'] - - def test_create_snapshot_check_mode(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - '_ansible_check_mode': True}, [ - ]) - assert result['changed'] is True - - def test_create_snapshot_with_comment(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - 'snapshot_comment': 'On Creation Comment'}, [ - FetchUrlCall('POST', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(LEGACY_CREATED_SNAPSHOT) - .expect_url(BASE_URL + '/storagebox/23/snapshot'), - FetchUrlCall("POST", 200) - .expect_url('{0}/storagebox/23/snapshot/{1}/comment'.format(BASE_URL, LEGACY_CREATED_SNAPSHOT['snapshot']['name'])) - .result_json({ - 'snapshot': { - 'name': '2025-03-28T15-20-51', - 'timestamp': '2025-03-28T16:20:51+01:00', - 'size': 0, - 'comment': 'On Creation Comment' - }}) - ]) - assert result['changed'] is True - assert result['snapshot']['comment'] == 'On Creation Comment' - - def test_comment_snapshot(self, mocker): - self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - 'snapshot_name': '2025-03-28T15-20-51', - 'snapshot_comment': 'Changing Comment', - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_EXISTING_SNAPSHOTS) - .expect_url(BASE_URL + '/storagebox/23/snapshot'), - FetchUrlCall('POST', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .expect_url('{0}/storagebox/23/snapshot/{1}/comment'.format(BASE_URL, '2025-03-28T15-20-51')) - ]) - - def test_same_comment_snapshot(self, mocker): - self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - 'snapshot_name': '2025-03-28T15-20-51', - 'snapshot_comment': 'Test-Snapshot 2', - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_EXISTING_SNAPSHOTS) - .expect_url(BASE_URL + '/storagebox/23/snapshot') - ]) - - def test_comment_snapshot_check_mode(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - 'snapshot_name': '2025-03-28T15-20-51', - 'snapshot_comment': 'Changing Comment', - '_ansible_check_mode': True - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_EXISTING_SNAPSHOTS) - .expect_url(BASE_URL + '/storagebox/23/snapshot'), - ]) - assert result['changed'] is True - def test_comment_snapshot_nonexistent_storagebox(self, mocker): self.run_module_failed(mocker, storagebox_snapshot, { 'hetzner_user': 'test', @@ -183,153 +58,8 @@ def test_comment_snapshot_nonexistent_storagebox(self, mocker): 'snapshot_name': '2025-03-28T15-20-51', 'snapshot_comment': 'Changing Comment', }, [ - FetchUrlCall('GET', 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'Storagebox with ID 54 does not exist', - } - }) - .expect_url(BASE_URL + '/storagebox/54/snapshot') ]) - def test_delete_snapshot(self, mocker): - self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - 'snapshot_name': '2025-03-28T15-20-51', - 'state': 'absent' - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_EXISTING_SNAPSHOTS) - .expect_url(BASE_URL + '/storagebox/23/snapshot'), - FetchUrlCall('DELETE', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .expect_url('{0}/storagebox/23/snapshot/{1}'.format(BASE_URL, '2025-03-28T15-20-51')) - ]) - - def test_delete_snapshot_check_mode(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - 'snapshot_name': '2025-03-28T15-20-51', - 'state': 'absent', - '_ansible_check_mode': True - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_EXISTING_SNAPSHOTS) - .expect_url(BASE_URL + '/storagebox/23/snapshot') - ]) - assert result['changed'] is True - - def test_create_limit_exceeded(self, mocker): - resutl = self.run_module_failed(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - }, [ - FetchUrlCall('POST', 409) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .expect_url('{0}/storagebox/23/snapshot'.format(BASE_URL)) - ]) - resutl['msg'] == 'Snapshot limit exceeded' - - def test_create_with_state_present(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - 'state': 'present'}, [ - FetchUrlCall('POST', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(LEGACY_CREATED_SNAPSHOT) - .expect_url(BASE_URL + '/storagebox/23/snapshot') - ]) - assert result['changed'] is True - assert result['snapshot'] == LEGACY_CREATED_SNAPSHOT['snapshot'] - - def test_delete_nonexistent_snapshot(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - 'snapshot_name': 'does-not-exist', - 'state': 'absent' - }, [ - FetchUrlCall("GET", 200) - .expect_url(BASE_URL + '/storagebox/23/snapshot') - .result_json(LEGACY_EXISTING_SNAPSHOTS) - ]) - assert result['changed'] is False - - def test_delete_snapshot_nonexistent_storagebox(self, mocker): - result = self.run_module_failed(mocker, storagebox_snapshot, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 54, - 'snapshot_name': '2025-03-28T15-20-51', - 'state': 'absent' - }, [ - FetchUrlCall("GET", 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'Storagebox with ID 54 does not exist', - } - }) - .expect_url(BASE_URL + '/storagebox/54/snapshot') - ]) - assert result['msg'] == 'Storagebox with ID 54 does not exist' - - def test_storagebox_id_unknown(self, mocker): - result = self.run_module_failed(mocker, storagebox_snapshot, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 54 - }, [ - FetchUrlCall('POST', 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'Storagebox with ID 54 does not exist', - } - }) - .expect_url(BASE_URL + '/storagebox/54/snapshot') - ]) - assert result['msg'] == 'Storagebox with ID 54 does not exist' - - def test_snapshot_name_unknown(self, mocker): - result = self.run_module_failed(mocker, storagebox_snapshot, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 23, - 'snapshot_name': '2038-01-19T03:14:17', - 'snapshot_comment': 'Test comment' - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_EXISTING_SNAPSHOTS) - .expect_url(BASE_URL + '/storagebox/23/snapshot') - ]) - assert result['msg'] == 'Snapshot with name 2038-01-19T03:14:17 does not exist' - - def test_snapshot_name_with_state_present(self, mocker): - result = self.run_module_failed(mocker, storagebox_snapshot, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 23, - 'snapshot_name': '2038-01-19T03:14:17', - 'state': 'present' - }, []) - assert result['msg'] == "snapshot_comment is required when updating a snapshot" - class TestHetznerStorageboxSnapshotPlanInfo(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox_snapshot.AnsibleModule' diff --git a/tests/unit/plugins/modules/test_storagebox_snapshot_info.py b/tests/unit/plugins/modules/test_storagebox_snapshot_info.py index d2fab094..0606af52 100644 --- a/tests/unit/plugins/modules/test_storagebox_snapshot_info.py +++ b/tests/unit/plugins/modules/test_storagebox_snapshot_info.py @@ -12,34 +12,9 @@ ) from ansible_collections.community.hrobot.plugins.module_utils.api import API_BASE_URL -from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import storagebox_snapshot_info -LEGACY_STORAGEBOX_SNAPSHOTS = [ - { - "snapshot": { - "name": "2015-12-21T12-40-38", - "timestamp": "2015-12-21T13:40:38+00:00", - "size": 400, - "filesystem_size": 12345, - "automatic": False, - "comment": "Test-Snapshot 1" - } - }, - { - "snapshot": { - "name": "2025-01-24T12-00-00", - "timestamp": "2025-01-24T12:00:00+00:00", - "size": 10000, - "filesystem_size": 22345, - "automatic": False, - "comment": "Test-Snapshot 2" - } - } -] - - STORAGEBOX_SNAPSHOTS = { "snapshots": [ { @@ -84,36 +59,12 @@ class TestHetznerStorageboxSnapshotPlanInfoLegacy(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox_snapshot_info.AnsibleModule' MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.hrobot.plugins.module_utils.robot.fetch_url' - def test_snapshot(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot_info, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23}, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_SNAPSHOTS) - .expect_url(BASE_URL + '/storagebox/23/snapshot') - ]) - assert result['changed'] is False - assert len(result['snapshots']) == 2 - assert result['snapshots'][0] == LEGACY_STORAGEBOX_SNAPSHOTS[0]['snapshot'] - def test_storagebox_id_unknown(self, mocker): result = self.run_module_failed(mocker, storagebox_snapshot_info, { 'hetzner_user': '', 'hetzner_password': '', 'storagebox_id': 23 }, [ - FetchUrlCall('GET', 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'Storage Box with ID 23 does not exist', - } - }) - .expect_url(BASE_URL + '/storagebox/23/snapshot') ]) assert result['msg'] == 'Storagebox with ID 23 does not exist' diff --git a/tests/unit/plugins/modules/test_storagebox_snapshot_plan.py b/tests/unit/plugins/modules/test_storagebox_snapshot_plan.py index 999f0a89..1ff495df 100644 --- a/tests/unit/plugins/modules/test_storagebox_snapshot_plan.py +++ b/tests/unit/plugins/modules/test_storagebox_snapshot_plan.py @@ -14,38 +14,9 @@ from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock from ansible_collections.community.hrobot.plugins.module_utils.api import API_BASE_URL -from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import storagebox_snapshot_plan -LEGACY_STORAGEBOX_PLAN_ENABLED = [ - { - 'snapshotplan': { - 'status': 'enabled', - 'minute': 5, - 'hour': 12, - 'day_of_week': 2, - 'day_of_month': None, - 'month': None, - 'max_snapshots': 2, - }, - }, -] - -LEGACY_STORAGEBOX_PLAN_DISABLED = [ - { - 'snapshotplan': { - 'status': 'disabled', - 'minute': None, - 'hour': None, - 'day_of_week': None, - 'day_of_month': None, - 'month': None, - 'max_snapshots': None, - }, - }, -] - STORAGEBOX_PLAN_ENABLED = { "storage_box": { 'id': 23, @@ -197,223 +168,29 @@ } -def legacy_update_plan(plan, **values): - def update(p): - p = dict(p) - p.update(values) - return p - - return [update(p) for p in plan] - - class TestHetznerStorageboxSnapshotPlanLegacy(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox_snapshot_plan.AnsibleModule' MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.hrobot.plugins.module_utils.robot.fetch_url' - def test_idempotent(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot_plan, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - 'plans': [ - LEGACY_STORAGEBOX_PLAN_ENABLED[0]['snapshotplan'], - ], - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_PLAN_ENABLED) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['plans']) == 1 - assert result['plans'][0] == LEGACY_STORAGEBOX_PLAN_ENABLED[0]['snapshotplan'] - def test_id_unknown(self, mocker): result = self.run_module_failed(mocker, storagebox_snapshot_plan, { 'hetzner_user': '', 'hetzner_password': '', 'storagebox_id': 1, - 'plans': [ - LEGACY_STORAGEBOX_PLAN_ENABLED[0]['snapshotplan'], - ], - }, [ - FetchUrlCall('GET', 404) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'Storagebox not found', - }, - }) - .expect_url('{0}/storagebox/1/snapshotplan'.format(BASE_URL)), - ]) - assert result['msg'] == 'Storagebox with ID 1 does not exist' - - def test_wrong_number_of_plans(self, mocker): - result = self.run_module_failed(mocker, storagebox_snapshot_plan, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 23, - 'plans': [], - }, [ - ]) - assert result['msg'] == '`plans` must have exactly one element' - - def test_invalid_input(self, mocker): - result = self.run_module_failed(mocker, storagebox_snapshot_plan, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 23, - 'plans': [ - { - 'status': 'enabled', - 'hour': 25, - 'minute': 0, - 'max_snapshots': -1, - }, - ], - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - # The actual API does not return a list, but its only entry directly - .result_json(LEGACY_STORAGEBOX_PLAN_ENABLED[0]) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - FetchUrlCall('POST', 400) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json({ - 'error': { - 'status': 400, - 'code': 'INVALID_INPUT', - 'message': 'Invalid input', - 'invalid': ['hour', 'max_snapshots'], - 'missing': None, - }, - }) - .expect_form_value('status', 'enabled') - .expect_form_value('hour', '25') - .expect_form_value('minute', '0') - .expect_form_value_absent('day_of_week') - .expect_form_value_absent('day_of_month') - .expect_form_value_absent('month') - .expect_form_value('max_snapshots', '-1') - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - ]) - assert result['msg'] == 'The values to update were invalid (hour, max_snapshots)' - - def test_disable(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot_plan, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 23, - 'plans': [ - { - 'status': 'disabled', - }, - ], - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_PLAN_ENABLED) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - FetchUrlCall('POST', 400) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_PLAN_DISABLED) - .expect_form_value('status', 'disabled') - .expect_form_value('hour', '0') # should be absent, but API does not permit that - .expect_form_value('minute', '0') # should be absent, but API does not permit that - .expect_form_value_absent('day_of_week') - .expect_form_value_absent('day_of_month') - .expect_form_value_absent('month') - .expect_form_value_absent('max_snapshots') - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - ]) - assert result['changed'] is True - assert len(result['plans']) == 1 - assert result['plans'][0] == LEGACY_STORAGEBOX_PLAN_DISABLED[0]['snapshotplan'] - - def test_enable(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot_plan, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 23, 'plans': [ { 'status': 'enabled', 'minute': 5, 'hour': 12, 'day_of_week': 2, - 'max_snapshots': 2, - }, - ], - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_PLAN_DISABLED) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - FetchUrlCall('POST', 400) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_PLAN_ENABLED) - .expect_form_value('status', 'enabled') - .expect_form_value('hour', '12') - .expect_form_value('minute', '5') - .expect_form_value('day_of_week', '2') - .expect_form_value_absent('day_of_month') - .expect_form_value_absent('month') - .expect_form_value('max_snapshots', '2') - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - ]) - assert result['changed'] is True - assert len(result['plans']) == 1 - assert result['plans'][0] == LEGACY_STORAGEBOX_PLAN_ENABLED[0]['snapshotplan'] - - def test_change(self, mocker): - updated_plan = legacy_update_plan(LEGACY_STORAGEBOX_PLAN_ENABLED, day_of_week=None, day_of_month=1) - result = self.run_module_success(mocker, storagebox_snapshot_plan, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 23, - 'plans': [ - { - 'status': 'enabled', - 'minute': 5, - 'hour': 12, - 'day_of_month': 1, + 'day_of_month': None, 'month': None, 'max_snapshots': 2, }, ], }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_PLAN_ENABLED) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - FetchUrlCall('POST', 400) - .expect_basic_auth('', '') - .expect_force_basic_auth(True) - # The actual API does not return a list, but its only entry directly - .result_json(updated_plan[0]) - .expect_form_value('status', 'enabled') - .expect_form_value('hour', '12') - .expect_form_value('minute', '5') - .expect_form_value_absent('day_of_week') - .expect_form_value('day_of_month', '1') - .expect_form_value_absent('month') - .expect_form_value('max_snapshots', '2') - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), ]) - assert result['changed'] is True - assert len(result['plans']) == 1 - assert result['plans'][0] == updated_plan[0]['snapshotplan'] + assert result['msg'] == 'Storagebox with ID 1 does not exist' class TestHetznerStorageboxSnapshotPlan(BaseTestModule): diff --git a/tests/unit/plugins/modules/test_storagebox_snapshot_plan_info.py b/tests/unit/plugins/modules/test_storagebox_snapshot_plan_info.py index 6f1448ba..c8b3bfdf 100644 --- a/tests/unit/plugins/modules/test_storagebox_snapshot_plan_info.py +++ b/tests/unit/plugins/modules/test_storagebox_snapshot_plan_info.py @@ -12,38 +12,9 @@ ) from ansible_collections.community.hrobot.plugins.module_utils.api import API_BASE_URL -from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import storagebox_snapshot_plan_info -LEGACY_STORAGEBOX_PLAN_ENABLED = [ - { - 'snapshotplan': { - 'status': 'enabled', - 'minute': 5, - 'hour': 12, - 'day_of_week': 2, - 'day_of_month': None, - 'month': None, - 'max_snapshots': 2, - }, - }, -] - -LEGACY_STORAGEBOX_PLAN_DISABLED = [ - { - 'snapshotplan': { - 'status': 'disabled', - 'minute': None, - 'hour': None, - 'day_of_week': None, - 'day_of_month': None, - 'month': None, - 'max_snapshots': None, - }, - }, -] - STORAGEBOX_PLAN_ENABLED = { "storage_box": { 'id': 23, @@ -199,87 +170,12 @@ class TestHetznerStorageboxSnapshotPlanInfoLegacy(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox_snapshot_plan_info.AnsibleModule' MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.hrobot.plugins.module_utils.robot.fetch_url' - def test_regular_enabled(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot_plan_info, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_PLAN_ENABLED) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['plans']) == 1 - assert result['plans'][0] == LEGACY_STORAGEBOX_PLAN_ENABLED[0]['snapshotplan'] - - def test_regular_disabled(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot_plan_info, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_PLAN_DISABLED) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['plans']) == 1 - assert result['plans'][0] == LEGACY_STORAGEBOX_PLAN_DISABLED[0]['snapshotplan'] - - def test_actual_enabled(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot_plan_info, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - # The actual API does not return a list, but its only entry directly - .result_json(LEGACY_STORAGEBOX_PLAN_ENABLED[0]) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['plans']) == 1 - assert result['plans'][0] == LEGACY_STORAGEBOX_PLAN_ENABLED[0]['snapshotplan'] - - def test_actual_disabled(self, mocker): - result = self.run_module_success(mocker, storagebox_snapshot_plan_info, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23, - }, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - # The actual API does not return a list, but its only entry directly - .result_json(LEGACY_STORAGEBOX_PLAN_DISABLED[0]) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), - ]) - assert result['changed'] is False - assert len(result['plans']) == 1 - assert result['plans'][0] == LEGACY_STORAGEBOX_PLAN_DISABLED[0]['snapshotplan'] - def test_server_number_unknown(self, mocker): result = self.run_module_failed(mocker, storagebox_snapshot_plan_info, { 'hetzner_user': '', 'hetzner_password': '', 'storagebox_id': 23, }, [ - FetchUrlCall('GET', 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'server not found', - }, - }) - .expect_url('{0}/storagebox/23/snapshotplan'.format(BASE_URL)), ]) assert result['msg'] == 'Storagebox with ID 23 does not exist' diff --git a/tests/unit/plugins/modules/test_storagebox_subaccount.py b/tests/unit/plugins/modules/test_storagebox_subaccount.py index b316b548..7d789ba9 100644 --- a/tests/unit/plugins/modules/test_storagebox_subaccount.py +++ b/tests/unit/plugins/modules/test_storagebox_subaccount.py @@ -15,45 +15,10 @@ from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock from ansible_collections.community.hrobot.plugins.module_utils.api import API_BASE_URL -from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import ( storagebox_subaccount, ) -LEGACY_STORAGEBOX_SUBACCOUNTS = [ - { - "subaccount": { - "username": "u2342-sub1", - "accountid": "u2342", - "server": "u12345-sub1.your-storagebox.de", - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "createtime": "2017-05-24 00:00:00", - "comment": "Test account", - } - }, - { - "subaccount": { - "username": "u2342-sub2", - "accountid": "u2342", - "server": "u12345-sub2.your-storagebox.de", - "homedirectory": "test2", - "samba": False, - "ssh": True, - "external_reachability": True, - "webdav": True, - "readonly": False, - "createtime": "2025-01-24 00:00:00", - "comment": "Test account 2", - } - }, -] - - STORAGEBOX_SUBACCOUNTS = [ { "id": 1, @@ -102,879 +67,9 @@ def test_storagebox_id_unknown(self, mocker): 'hetzner_password': '', 'storagebox_id': 23, }, [ - FetchUrlCall('GET', 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'Storage Box with ID 23 does not exist', - } - }) - .expect_url(BASE_URL + '/storagebox/23/subaccount') ]) assert result['msg'] == 'Storagebox with ID 23 does not exist' - def test_delete_unknown_subaccount_noop(self, mocker): - """Test deletion of a subaccount that doesn't exist (no-op).""" - result = self.run_module_success(mocker, storagebox_subaccount, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - 'state': 'absent', - 'username': 'ghost_user', - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - ]) - - assert result['changed'] is False - assert result['created'] is False - assert result['deleted'] is False - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] is None - - def test_delete_existing_subaccount(self, mocker): - """Test successful deletion of an existing subaccount.""" - result = self.run_module_success(mocker, storagebox_subaccount, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - 'state': 'absent', - 'username': 'u2342-sub2', - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('DELETE', 200) - .result_json({}) - .expect_url(BASE_URL + '/storagebox/1234/subaccount/u2342-sub2') - ]) - - assert result['changed'] is True - assert result['created'] is False - assert result['deleted'] is True - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] is None - - def test_delete_existing_subaccount_idempotence_by_comment(self, mocker): - """Test successful deletion of an existing subaccount.""" - result = self.run_module_success(mocker, storagebox_subaccount, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - 'state': 'absent', - 'comment': 'Test account', - 'idempotence': 'comment' - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('DELETE', 200) - .result_json({}) - .expect_url(BASE_URL + '/storagebox/1234/subaccount/u2342-sub1') - ]) - - assert result['changed'] is True - assert result['created'] is False - assert result['deleted'] is True - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] is None - - def test_create_subaccount(self, mocker): - result = self.run_module_success( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount - 'homedirectory': '/data/newsub', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'My new subaccount' - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('POST', 200) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - .expect_form_value('homedirectory', '/data/newsub') - .expect_form_value('samba', 'true') - .expect_form_value('ssh', 'false') - .expect_form_value('webdav', 'true') - .expect_form_value('readonly', 'false') - .expect_form_value('comment', 'My new subaccount') - .result_json({ - 'subaccount': { - 'username': 'generated_user', - 'homedirectory': '/data/newsub', - 'password': 'autogeneratedpass123' - } - }), - ] - ) - - assert result['changed'] is True - assert result['created'] is True - assert result['deleted'] is False - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] == { - 'username': 'generated_user', - 'homedirectory': '/data/newsub', - 'password': 'autogeneratedpass123', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'My new subaccount', - } - - # Ensures providing a username doesn't trigger accidental update (if username isn't known) - def test_create_subaccount_unknown_username(self, mocker): - result = self.run_module_success( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount data - 'homedirectory': '/data/newsub', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'My new subaccount', - 'username': "I'll be ignored", - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('POST', 200) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - .expect_form_value('homedirectory', '/data/newsub') - .expect_form_value('samba', 'true') - .expect_form_value('ssh', 'false') - .expect_form_value('webdav', 'true') - .expect_form_value('readonly', 'false') - .expect_form_value('comment', 'My new subaccount') - .result_json({ - 'subaccount': { - 'username': 'generated_user', - 'homedirectory': '/data/newsub', - 'password': 'autogeneratedpass123' - } - }), - ] - ) - - assert result['changed'] is True - assert result['created'] is True - assert result['deleted'] is False - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] == { - 'username': 'generated_user', - 'homedirectory': '/data/newsub', - 'password': 'autogeneratedpass123', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'My new subaccount', - } - - def test_create_subaccount_set_to_random(self, mocker): - result = self.run_module_success( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - 'password_mode': 'set-to-random', - # subaccount - 'homedirectory': '/data/newsub', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'My new subaccount', - 'password': 'toto' - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('POST', 200) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - .expect_form_value('homedirectory', '/data/newsub') - .expect_form_value('samba', 'true') - .expect_form_value('ssh', 'false') - .expect_form_value('webdav', 'true') - .expect_form_value('readonly', 'false') - .expect_form_value('comment', 'My new subaccount') - .expect_form_value_absent('password') - .result_json({ - 'subaccount': { - 'username': 'generated_user', - 'homedirectory': '/data/newsub', - 'password': 'autogeneratedpass123' - } - }), - ] - ) - - assert result['changed'] is True - assert result['created'] is True - assert result['deleted'] is False - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] == { - 'username': 'generated_user', - 'homedirectory': '/data/newsub', - 'password': 'autogeneratedpass123', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'My new subaccount', - } - - def test_create_subaccount_limit_exceeded(self, mocker): - result = self.run_module_failed( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount - 'homedirectory': '/data/newsub', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'My new subaccount' - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('POST', 400) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - .expect_form_value('homedirectory', '/data/newsub') - .expect_form_value('samba', 'true') - .expect_form_value('ssh', 'false') - .expect_form_value('webdav', 'true') - .expect_form_value('readonly', 'false') - .expect_form_value('comment', 'My new subaccount') - .result_json({ - "error": { - "status": 400, - "code": "STORAGEBOX_SUBACCOUNT_LIMIT_EXCEEDED", - "message": "Too many requests" - } - }) - ] - ) - assert result['msg'] == "Subaccount limit exceeded" - - def test_create_subaccount_invalid_password(self, mocker): - result = self.run_module_failed( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount - 'homedirectory': '/data/newsub', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'Invalid password attempt', - 'password': '123' - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('POST', 400) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - .expect_form_value("password", "123") - .result_json({ - "error": { - "status": 400, - "code": "STORAGEBOX_INVALID_PASSWORD", - "message": "Password does not meet security requirements" - } - }) - ] - ) - - assert result['msg'] == "Invalid password (says Hetzner)" - - def test_update_subaccount_noop(self, mocker): - result = self.run_module_success( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount - "username": "u2342-sub1", - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "comment": "Test account", - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - ] - ) - - assert result['changed'] is False - assert result['created'] is False - assert result['deleted'] is False - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] == { - 'username': 'u2342-sub1', - 'accountid': 'u2342', - 'server': 'u12345-sub1.your-storagebox.de', - 'homedirectory': 'test', - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'webdav': False, - 'readonly': False, - 'createtime': '2017-05-24 00:00:00', - 'comment': 'Test account', - } - - def test_update_subaccount_noop_idempotence_by_comment(self, mocker): - result = self.run_module_success( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount - 'idempotence': 'comment', - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "comment": "Test account", - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - ] - ) - - assert result['changed'] is False - assert result['created'] is False - assert result['deleted'] is False - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] == { - 'username': 'u2342-sub1', - 'accountid': 'u2342', - 'server': 'u12345-sub1.your-storagebox.de', - 'homedirectory': 'test', - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'webdav': False, - 'readonly': False, - 'createtime': '2017-05-24 00:00:00', - 'comment': 'Test account', - } - - def test_update_subaccount(self, mocker): - input = { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount - "username": "u2342-sub1", - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "comment": "new comment", - } - result = self.run_module_success( - mocker, - storagebox_subaccount, - input, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('PUT', 200) - .expect_url(BASE_URL + '/storagebox/1234/subaccount/' + input['username']) - .expect_form_value("homedirectory", input["homedirectory"]) - .expect_form_value("samba", str(input["samba"]).lower()) - .expect_form_value("ssh", str(input["ssh"]).lower()) - .expect_form_value("external_reachability", str(input["external_reachability"]).lower()) - .expect_form_value("webdav", str(input["webdav"]).lower()) - .expect_form_value("readonly", str(input["readonly"]).lower()) - .expect_form_value("comment", input["comment"]) - .result_json({}) - ] - ) - - assert result['changed'] is True - assert result['created'] is False - assert result['deleted'] is False - assert result['updated'] is True - assert result['password_updated'] is False - assert result['subaccount'] == { - 'username': 'u2342-sub1', - 'accountid': 'u2342', - 'server': 'u12345-sub1.your-storagebox.de', - 'homedirectory': 'test', - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'webdav': False, - 'readonly': False, - 'createtime': '2017-05-24 00:00:00', - 'comment': 'new comment', - } - - def test_update_subaccount_idempotence_by_comment(self, mocker): - input = { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount - 'idempotence': 'comment', - "homedirectory": "/new/homedir", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "comment": "Test account", - } - result = self.run_module_success( - mocker, - storagebox_subaccount, - input, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('PUT', 200) - .expect_url(BASE_URL + '/storagebox/1234/subaccount/' + LEGACY_STORAGEBOX_SUBACCOUNTS[0]['subaccount']['username']) - .expect_form_value("homedirectory", input["homedirectory"]) - .expect_form_value("samba", str(input["samba"]).lower()) - .expect_form_value("ssh", str(input["ssh"]).lower()) - .expect_form_value("external_reachability", str(input["external_reachability"]).lower()) - .expect_form_value("webdav", str(input["webdav"]).lower()) - .expect_form_value("readonly", str(input["readonly"]).lower()) - .expect_form_value("comment", input["comment"]) - .result_json({}) - ] - ) - - assert result['changed'] is True - assert result['created'] is False - assert result['deleted'] is False - assert result['updated'] is True - assert result['password_updated'] is False - assert result['subaccount'] == { - 'username': 'u2342-sub1', - 'accountid': 'u2342', - 'server': 'u12345-sub1.your-storagebox.de', - 'homedirectory': '/new/homedir', - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'webdav': False, - 'readonly': False, - 'createtime': '2017-05-24 00:00:00', - 'comment': 'Test account', - } - - def test_update_subaccount_set_to_random(self, mocker): - input = { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount - "username": "u2342-sub1", - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "comment": "new comment", - "password": "toto", - "password_mode": "set-to-random", - } - result = self.run_module_success( - mocker, - storagebox_subaccount, - input, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('POST', 200) - .expect_url(BASE_URL + '/storagebox/1234/subaccount/' + input['username'] + '/password') - .expect_form_value_absent("password") - .result_json({"password": "newRandomPassword"}), - - FetchUrlCall('PUT', 200) - .expect_url(BASE_URL + '/storagebox/1234/subaccount/' + input['username']) - .expect_form_value("homedirectory", input["homedirectory"]) - .expect_form_value("samba", str(input["samba"]).lower()) - .expect_form_value("ssh", str(input["ssh"]).lower()) - .expect_form_value("external_reachability", str(input["external_reachability"]).lower()) - .expect_form_value("webdav", str(input["webdav"]).lower()) - .expect_form_value("readonly", str(input["readonly"]).lower()) - .expect_form_value("comment", input["comment"]) - .result_json({}) - ] - ) - - assert result['changed'] is True - assert result['created'] is False - assert result['deleted'] is False - assert result['updated'] is True - assert result['password_updated'] is True - assert result['subaccount'] == { - 'username': 'u2342-sub1', - 'accountid': 'u2342', - 'server': 'u12345-sub1.your-storagebox.de', - 'homedirectory': 'test', - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'webdav': False, - 'readonly': False, - 'createtime': '2017-05-24 00:00:00', - 'comment': 'new comment', - 'password': 'newRandomPassword', - } - - def test_update_password_only(self, mocker): - result = self.run_module_success( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # account - "password": "newsecurepassword", - "username": "u2342-sub1", - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "comment": "Test account", - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('POST', 200) - .expect_url(BASE_URL + '/storagebox/1234/subaccount/u2342-sub1/password') - .expect_form_value("password", "newsecurepassword") - .result_json({"password": "newsecurepassword"}) - ] - ) - - assert result['changed'] is True - assert result['created'] is False - assert result['deleted'] is False - assert result['updated'] is False - assert result['password_updated'] is True - assert result['subaccount'] == { - 'username': 'u2342-sub1', - 'accountid': 'u2342', - 'server': 'u12345-sub1.your-storagebox.de', - 'homedirectory': 'test', - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'webdav': False, - 'readonly': False, - 'createtime': '2017-05-24 00:00:00', - 'comment': 'Test account', - 'password': 'newsecurepassword', - } - - def test_update_subaccount_invalid_password(self, mocker): - result = self.run_module_failed( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount - "password": "123", - "username": "u2342-sub1", - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "comment": "Test account", - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount'), - - FetchUrlCall('POST', 400) - .expect_url(BASE_URL + '/storagebox/1234/subaccount/u2342-sub1/password') - .expect_form_value("password", "123") - .result_json({ - "error": { - "status": 400, - "code": "STORAGEBOX_INVALID_PASSWORD", - "message": "Password does not meet security requirements" - } - }) - ] - ) - - assert result['msg'] == "Invalid password (says Hetzner)" - - def test_invalid_spec_create_no_homedir(self, mocker): - result = self.run_module_failed( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - 'state': 'present', - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - ] - ) - - assert result['msg'] == "homedirectory is required when creating a new subaccount" - - # Check mode tests - def test_update_password_only_CHECK_MODE(self, mocker): - result = self.run_module_success( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - '_ansible_check_mode': True, - # subaccount - "password": "newsecurepassword", - "username": "u2342-sub1", - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "comment": "Test account", - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - ] - ) - - assert result['changed'] is True - assert result['created'] is False - assert result['deleted'] is False - assert result['updated'] is False - assert result['password_updated'] is True - assert result['subaccount'] == { - 'username': 'u2342-sub1', - 'accountid': 'u2342', - 'server': 'u12345-sub1.your-storagebox.de', - 'homedirectory': 'test', - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'webdav': False, - 'readonly': False, - 'createtime': '2017-05-24 00:00:00', - 'comment': 'Test account', - 'password': 'newsecurepassword', - } - - def test_create_subaccount_CHECK_MODE(self, mocker): - result = self.run_module_success( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - # subaccount, - 'homedirectory': '/data/newsub', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'My new subaccount', - '_ansible_check_mode': True, - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - ] - ) - - assert result['changed'] is True - assert result['created'] is True - assert result['deleted'] is False - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] == { - 'homedirectory': '/data/newsub', - 'samba': True, - 'ssh': False, - 'webdav': True, - 'readonly': False, - 'comment': 'My new subaccount', - } - - def test_update_subaccount_CHECK_MODE(self, mocker): - result = self.run_module_success( - mocker, - storagebox_subaccount, - { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - '_ansible_check_mode': True, - # subaccount - "username": "u2342-sub1", - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "comment": "new comment", - - }, - [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - ] - ) - - assert result['changed'] is True - assert result['created'] is False - assert result['deleted'] is False - assert result['updated'] is True - assert result['password_updated'] is False - assert result['subaccount'] == { - 'username': 'u2342-sub1', - 'accountid': 'u2342', - 'server': 'u12345-sub1.your-storagebox.de', - 'homedirectory': 'test', - 'samba': True, - 'ssh': True, - 'external_reachability': True, - 'webdav': False, - 'readonly': False, - 'createtime': '2017-05-24 00:00:00', - 'comment': 'new comment', - } - - def test_delete_existing_subaccount_CHECK_MODE(self, mocker): - """Test successful deletion of an existing subaccount.""" - result = self.run_module_success(mocker, storagebox_subaccount, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - 'state': 'absent', - 'username': 'u2342-sub2', - '_ansible_check_mode': True, - }, [ - FetchUrlCall('GET', 200) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - ]) - - assert result['changed'] is True - assert result['created'] is False - assert result['deleted'] is True - assert result['updated'] is False - assert result['password_updated'] is False - assert result['subaccount'] is None - - def test_broken_idempotence_same_comment_multiple_accounts(self, mocker): - result = self.run_module_failed(mocker, storagebox_subaccount, { - 'hetzner_user': '', - 'hetzner_password': '', - 'storagebox_id': 1234, - 'idempotence': 'comment', - 'comment': LEGACY_STORAGEBOX_SUBACCOUNTS[0]['subaccount']['comment'], - }, [ - FetchUrlCall('GET', 200) - .result_json([LEGACY_STORAGEBOX_SUBACCOUNTS[0], LEGACY_STORAGEBOX_SUBACCOUNTS[0]]) - .expect_url(BASE_URL + '/storagebox/1234/subaccount') - ]) - - assert result['msg'] == "More than one subaccount matched the idempotence criteria." - class TestHetznerStorageboxSubbacount(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox_subaccount.AnsibleModule' diff --git a/tests/unit/plugins/modules/test_storagebox_subaccount_info.py b/tests/unit/plugins/modules/test_storagebox_subaccount_info.py index f43fad8a..643ade82 100644 --- a/tests/unit/plugins/modules/test_storagebox_subaccount_info.py +++ b/tests/unit/plugins/modules/test_storagebox_subaccount_info.py @@ -13,44 +13,10 @@ ) from ansible_collections.community.hrobot.plugins.module_utils.api import API_BASE_URL -from ansible_collections.community.hrobot.plugins.module_utils.robot import BASE_URL from ansible_collections.community.hrobot.plugins.modules import ( storagebox_subaccount_info, ) -LEGACY_STORAGEBOX_SUBACCOUNTS = [ - { - "subaccount": { - "username": "u2342-sub1", - "accountid": "u2342", - "server": "u12345-sub1.your-storagebox.de", - "homedirectory": "test", - "samba": True, - "ssh": True, - "external_reachability": True, - "webdav": False, - "readonly": False, - "createtime": "2017-05-24 00:00:00", - "comment": "Test account", - } - }, - { - "subaccount": { - "username": "u2342-sub2", - "accountid": "u2342", - "server": "u12345-sub2.your-storagebox.de", - "homedirectory": "test2", - "samba": False, - "ssh": True, - "external_reachability": True, - "webdav": True, - "readonly": False, - "createtime": "2025-01-24 00:00:00", - "comment": "Test account 2", - } - }, -] - STORAGEBOX_SUBACCOUNTS = { "subaccounts": [ { @@ -82,36 +48,12 @@ class TestHetznerStorageboxSubbacountInfoLegacy(BaseTestModule): MOCK_ANSIBLE_MODULEUTILS_BASIC_ANSIBLEMODULE = 'ansible_collections.community.hrobot.plugins.modules.storagebox_subaccount_info.AnsibleModule' MOCK_ANSIBLE_MODULEUTILS_URLS_FETCH_URL = 'ansible_collections.community.hrobot.plugins.module_utils.robot.fetch_url' - def test_subaccounts(self, mocker): - result = self.run_module_success(mocker, storagebox_subaccount_info, { - 'hetzner_user': 'test', - 'hetzner_password': 'hunter2', - 'storagebox_id': 23}, [ - FetchUrlCall('GET', 200) - .expect_basic_auth('test', 'hunter2') - .expect_force_basic_auth(True) - .result_json(LEGACY_STORAGEBOX_SUBACCOUNTS) - .expect_url(BASE_URL + '/storagebox/23/subaccount') - ]) - assert result['changed'] is False - assert len(result['subaccounts']) == 2 - assert result['subaccounts'][0] == LEGACY_STORAGEBOX_SUBACCOUNTS[0]['subaccount'] - def test_storagebox_id_unknown(self, mocker): result = self.run_module_failed(mocker, storagebox_subaccount_info, { 'hetzner_user': '', 'hetzner_password': '', 'storagebox_id': 23 }, [ - FetchUrlCall('GET', 404) - .result_json({ - 'error': { - 'status': 404, - 'code': 'STORAGEBOX_NOT_FOUND', - 'message': 'Storage Box with ID 23 does not exist', - } - }) - .expect_url(BASE_URL + '/storagebox/23/subaccount') ]) assert result['msg'] == 'Storagebox with ID 23 does not exist'