summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/hrobot/plugins/modules
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/hrobot/plugins/modules
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/hrobot/plugins/modules')
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/boot.py444
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/failover_ip.py154
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/failover_ip_info.py127
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/firewall.py704
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/firewall_info.py318
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/reset.py150
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/reverse_dns.py133
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/server.py274
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/server_info.py283
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/ssh_key.py245
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/ssh_key_info.py113
-rw-r--r--ansible_collections/community/hrobot/plugins/modules/v_switch.py504
12 files changed, 3449 insertions, 0 deletions
diff --git a/ansible_collections/community/hrobot/plugins/modules/boot.py b/ansible_collections/community/hrobot/plugins/modules/boot.py
new file mode 100644
index 000000000..64917d9b8
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/boot.py
@@ -0,0 +1,444 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: boot
+short_description: Set boot configuration
+version_added: 1.2.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Set the boot configuration for a dedicated server.
+seealso:
+ - module: community.hrobot.ssh_key
+ description: Add, remove or update SSH key
+ - module: community.hrobot.ssh_key_info
+ description: Query information on SSH keys
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+
+options:
+ server_number:
+ description:
+ - The server number of the server whose boot configuration to adjust.
+ type: int
+ required: true
+ regular_boot:
+ description:
+ - If this option is provided, all special boot configurations are removed and
+ the installed operating system will be booted up next (assuming it is bootable).
+ - Precisely one of I(regular_boot), I(rescue), I(install_linux), I(install_vnc),
+ I(install_windows), I(install_plesk), and I(install_cpanel) must be provided.
+ type: bool
+ choices:
+ - true
+ rescue:
+ description:
+ - If this option is provided, the rescue system will be activated for the next boot.
+ - Precisely one of I(regular_boot), I(rescue), I(install_linux), I(install_vnc),
+ I(install_windows), I(install_plesk), and I(install_cpanel) must be provided.
+ type: dict
+ suboptions:
+ os:
+ description:
+ - The operating system to use for the rescue system. Possible choices can
+ change over time.
+ - Currently, C(linux), C(linuxold), C(freebsd), C(freebsdold), C(freebsdax),
+ C(freebsdbetaax), C(vkvm), and C(vkvmold) seem to be available.
+ type: str
+ required: true
+ arch:
+ description:
+ - The architecture to use for the rescue system.
+ - Not all architectures are available for all operating systems.
+ - Defaults to C(64).
+ type: int
+ choices:
+ - 32
+ - 64
+ authorized_keys:
+ description:
+ - One or more SSH key fingerprints to equip the rescue system with.
+ - Only fingerprints for SSH keys deposited in the Robot API can be used.
+ - You can use the M(community.hrobot.ssh_key_info) module to query the
+ SSH keys you can use, and the M(community.hrobot.ssh_key) module to
+ add or update SSH keys.
+ type: list
+ elements: str
+ install_linux:
+ description:
+ - If this option is provided, a Linux system install will be activated for the next boot.
+ - Precisely one of I(regular_boot), I(rescue), I(install_linux), I(install_vnc),
+ I(install_windows), I(install_plesk), and I(install_cpanel) must be provided.
+ type: dict
+ suboptions:
+ dist:
+ description:
+ - The distribution to install.
+ type: str
+ required: true
+ arch:
+ description:
+ - The architecture to use for the install.
+ - Not all architectures are available for all distributions.
+ - Defaults to C(64).
+ type: int
+ choices:
+ - 32
+ - 64
+ lang:
+ description:
+ - The language to use for the operating system.
+ type: str
+ required: true
+ authorized_keys:
+ description:
+ - One or more SSH key fingerprints to equip the rescue system with.
+ - Only fingerprints for SSH keys deposited in the Robot API can be used.
+ - You can use the M(community.hrobot.ssh_key_info) module to query the
+ SSH keys you can use, and the M(community.hrobot.ssh_key) module to
+ add or update SSH keys.
+ type: list
+ elements: str
+ install_vnc:
+ description:
+ - If this option is provided, a VNC installation will be activated for the next boot.
+ - Precisely one of I(regular_boot), I(rescue), I(install_linux), I(install_vnc),
+ I(install_windows), I(install_plesk), and I(install_cpanel) must be provided.
+ type: dict
+ suboptions:
+ dist:
+ description:
+ - The distribution to install.
+ type: str
+ required: true
+ arch:
+ description:
+ - The architecture to use for the install.
+ - Not all architectures are available for all distributions.
+ - Defaults to C(64).
+ type: int
+ choices:
+ - 32
+ - 64
+ lang:
+ description:
+ - The language to use for the operating system.
+ type: str
+ required: true
+ install_windows:
+ description:
+ - If this option is provided, a Windows installation will be activated for the next boot.
+ - Precisely one of I(regular_boot), I(rescue), I(install_linux), I(install_vnc),
+ I(install_windows), I(install_plesk), and I(install_cpanel) must be provided.
+ type: dict
+ suboptions:
+ lang:
+ description:
+ - The language to use for Windows.
+ type: str
+ required: true
+ install_plesk:
+ description:
+ - If this option is provided, a Plesk installation will be activated for the next boot.
+ - Precisely one of I(regular_boot), I(rescue), I(install_linux), I(install_vnc),
+ I(install_windows), I(install_plesk), and I(install_cpanel) must be provided.
+ type: dict
+ suboptions:
+ dist:
+ description:
+ - The distribution to install.
+ type: str
+ required: true
+ arch:
+ description:
+ - The architecture to use for the install.
+ - Not all architectures are available for all distributions.
+ - Defaults to C(64).
+ type: int
+ choices:
+ - 32
+ - 64
+ lang:
+ description:
+ - The language to use for the operating system.
+ type: str
+ required: true
+ hostname:
+ description:
+ - The hostname.
+ type: str
+ required: true
+ install_cpanel:
+ description:
+ - If this option is provided, a cPanel installation will be activated for the next boot.
+ - Precisely one of I(regular_boot), I(rescue), I(install_linux), I(install_vnc),
+ I(install_windows), I(install_plesk), and I(install_cpanel) must be provided.
+ type: dict
+ suboptions:
+ dist:
+ description:
+ - The distribution to install.
+ type: str
+ required: true
+ arch:
+ description:
+ - The architecture to use for the install.
+ - Not all architectures are available for all distributions.
+ - Defaults to C(64).
+ type: int
+ choices:
+ - 32
+ - 64
+ lang:
+ description:
+ - The language to use for the operating system.
+ type: str
+ required: true
+ hostname:
+ description:
+ - The hostname.
+ type: str
+ required: true
+'''
+
+EXAMPLES = r'''
+- name: Disable all special boot configurations
+ community.hrobot.boot:
+ hetzner_user: foo
+ hetzner_password: bar
+ regular_boot: true
+
+- name: Enable a rescue system (64bit Linux) for the next boot
+ community.hrobot.boot:
+ hetzner_user: foo
+ hetzner_password: bar
+ rescue:
+ os: linux
+
+- name: Enable a Linux install for the next boot
+ community.hrobot.boot:
+ hetzner_user: foo
+ hetzner_password: bar
+ install_linux:
+ dist: CentOS 5.5 minimal
+ lang: en
+ authorized_keys:
+ - 56:29:99:a4:5d:ed:ac:95:c1:f5:88:82:90:5d:dd:10
+ - 15:28:b0:03:95:f0:77:b3:10:56:15:6b:77:22:a5:bb
+'''
+
+RETURN = r'''
+configuration_type:
+ description:
+ - Describes the active boot configuration.
+ returned: success
+ type: str
+ choices:
+ - regular_boot
+ - rescue
+ - install_linux
+ - install_vnc
+ - install_windows
+ - install_plesk
+ - install_cpanel
+password:
+ description:
+ - The root password for the active boot configuration, if available.
+ - For non-rescue boot configurations, it is avised to change the root password
+ as soon as possible.
+ returned: success and if a boot configuration other than C(regular_boot) is active
+ type: str
+'''
+
+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,
+ fetch_url_json,
+)
+
+
+BOOT_CONFIGURATION_DATA = [
+ ('rescue', 'rescue', {
+ 'os': ('os', 'os'),
+ 'arch': ('arch', 'arch'),
+ 'authorized_keys': ('authorized_key', 'authorized_key'),
+ }),
+ ('install_linux', 'linux', {
+ 'dist': ('dist', 'dist'),
+ 'arch': ('arch', 'arch'),
+ 'lang': ('lang', 'lang'),
+ 'authorized_keys': ('authorized_key', 'authorized_key'),
+ }),
+ ('install_vnc', 'vnc', {
+ 'dist': ('dist', 'dist'),
+ 'arch': ('arch', 'arch'),
+ 'lang': ('lang', 'lang'),
+ }),
+ ('install_windows', 'windows', {
+ 'lang': ('lang', 'lang'),
+ }),
+ ('install_plesk', 'plesk', {
+ 'dist': ('dist', 'dist'),
+ 'arch': ('arch', 'arch'),
+ 'lang': ('lang', 'lang'),
+ 'hostname': ('hostname', 'hostname'),
+ }),
+ ('install_cpanel', 'cpanel', {
+ 'dist': ('dist', 'dist'),
+ 'arch': ('arch', 'arch'),
+ 'lang': ('lang', 'lang'),
+ 'hostname': ('hostname', 'hostname'),
+ }),
+]
+
+
+def main():
+ argument_spec = dict(
+ server_number=dict(type='int', required=True),
+ regular_boot=dict(type='bool', choices=[True]),
+ rescue=dict(type='dict', options=dict(
+ os=dict(type='str', required=True),
+ arch=dict(type='int', choices=[32, 64]),
+ authorized_keys=dict(type='list', elements='str', no_log=False),
+ )),
+ install_linux=dict(type='dict', options=dict(
+ dist=dict(type='str', required=True),
+ arch=dict(type='int', choices=[32, 64]),
+ lang=dict(type='str', required=True),
+ authorized_keys=dict(type='list', elements='str', no_log=False),
+ )),
+ install_vnc=dict(type='dict', options=dict(
+ dist=dict(type='str', required=True),
+ arch=dict(type='int', choices=[32, 64]),
+ lang=dict(type='str', required=True),
+ )),
+ install_windows=dict(type='dict', options=dict(
+ lang=dict(type='str', required=True),
+ )),
+ install_plesk=dict(type='dict', options=dict(
+ dist=dict(type='str', required=True),
+ arch=dict(type='int', choices=[32, 64]),
+ lang=dict(type='str', required=True),
+ hostname=dict(type='str', required=True),
+ )),
+ install_cpanel=dict(type='dict', options=dict(
+ dist=dict(type='str', required=True),
+ arch=dict(type='int', choices=[32, 64]),
+ lang=dict(type='str', required=True),
+ hostname=dict(type='str', required=True),
+ )),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ mutually_exclusive=[('regular_boot', 'rescue', 'install_linux', 'install_vnc', 'install_windows', 'install_plesk', 'install_cpanel')],
+ required_one_of=[('regular_boot', 'rescue', 'install_linux', 'install_vnc', 'install_windows', 'install_plesk', 'install_cpanel')],
+ )
+
+ server_number = module.params['server_number']
+ changed = False
+
+ # Retrieve current boot config
+ url = "{0}/boot/{1}".format(BASE_URL, server_number)
+ result, error = fetch_url_json(module, url, accept_errors=['SERVER_NOT_FOUND', 'BOOT_NOT_AVAILABLE'])
+ if error is not None:
+ if error == 'SERVER_NOT_FOUND':
+ module.fail_json(msg='This server does not exist, or you do not have access rights for it')
+ if error == 'BOOT_NOT_AVAILABLE':
+ module.fail_json(msg='There is no boot configuration available for this server')
+ raise AssertionError('Unexpected error {0}'.format(error)) # pragma: no cover
+
+ # Deactivate current boot configurations that are not requested
+ for option_name, other_name, dummy in BOOT_CONFIGURATION_DATA:
+ if (result['boot'].get(other_name) or {}).get('active') and not module.params[option_name]:
+ changed = True
+ if not module.check_mode:
+ url = "{0}/boot/{1}/{2}".format(BASE_URL, server_number, other_name)
+ fetch_url_json(module, url, method='DELETE', allow_empty_result=True)
+
+ # Enable/compare boot configuration
+ return_values = {
+ 'configuration_type': 'regular_boot',
+ 'password': None,
+ }
+ for option_name, other_name, options in BOOT_CONFIGURATION_DATA:
+ if module.params[option_name]:
+ return_values['configuration_type'] = option_name
+ existing = result['boot'].get(other_name) or {}
+ return_values['password'] = existing.get('password')
+ data = {}
+ for option_key, (result_key, data_key) in options.items():
+ option = module.params[option_name][option_key]
+ if option is None or option == []:
+ continue
+ data[data_key] = option
+ if existing.get('active'):
+ # Idempotence check
+ needs_change = False
+ for option_key, (result_key, data_key) in options.items():
+ should = module.params[option_name][option_key]
+ if should is None:
+ continue
+ # unfold the return object for the idempotence check to work correctly
+ has = existing.get(data_key)
+ if has and option_key == 'authorized_keys':
+ has = [x['key']['fingerprint'] for x in has]
+ if isinstance(has, list):
+ has = sorted(has)
+ if not isinstance(should, list):
+ should = [should]
+ should = sorted(should)
+ if should != has:
+ needs_change = True
+ else:
+ needs_change = True
+
+ if needs_change:
+ changed = True
+ if not module.check_mode:
+ url = "{0}/boot/{1}/{2}".format(BASE_URL, server_number, other_name)
+ if existing.get('active'):
+ # Deactivate existing boot configuration
+ fetch_url_json(module, url, method='DELETE', allow_empty_result=True)
+ # Enable new boot configuration
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
+ result, dummy = fetch_url_json(
+ module,
+ url,
+ data=urlencode(data, True),
+ headers=headers,
+ method='POST',
+ )
+ return_values['password'] = (result.get(other_name) or {}).get('password')
+ else:
+ return_values['password'] = None
+
+ module.exit_json(changed=changed, **return_values)
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/failover_ip.py b/ansible_collections/community/hrobot/plugins/modules/failover_ip.py
new file mode 100644
index 000000000..da2da356a
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/failover_ip.py
@@ -0,0 +1,154 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: failover_ip
+short_description: Manage Hetzner's failover IPs
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Manage Hetzner's failover IPs.
+seealso:
+ - name: Failover IP documentation
+ description: Hetzner's documentation on failover IPs.
+ link: https://docs.hetzner.com/robot/dedicated-server/ip/failover/
+ - module: community.hrobot.failover_ip_info
+ description: Retrieve information on failover IPs.
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+
+options:
+ failover_ip:
+ description: The failover IP address.
+ type: str
+ required: true
+ state:
+ description:
+ - Defines whether the IP will be routed or not.
+ - If set to C(routed), I(value) must be specified.
+ type: str
+ choices:
+ - routed
+ - unrouted
+ default: routed
+ value:
+ description:
+ - The new value for the failover IP address.
+ - Required when setting I(state) to C(routed).
+ type: str
+ timeout:
+ description:
+ - Timeout to use when routing or unrouting the failover IP.
+ - Note that the API call returns when the failover IP has been
+ successfully routed to the new address, respectively successfully
+ unrouted.
+ type: int
+ default: 180
+'''
+
+EXAMPLES = r'''
+- name: Set value of failover IP 1.2.3.4 to 5.6.7.8
+ community.hrobot.failover_ip:
+ hetzner_user: foo
+ hetzner_password: bar
+ failover_ip: 1.2.3.4
+ value: 5.6.7.8
+
+- name: Set value of failover IP 1.2.3.4 to unrouted
+ community.hrobot.failover_ip:
+ hetzner_user: foo
+ hetzner_password: bar
+ failover_ip: 1.2.3.4
+ state: unrouted
+'''
+
+RETURN = r'''
+value:
+ description:
+ - The value of the failover IP.
+ - Will be C(none) if the IP is unrouted.
+ returned: success
+ type: str
+state:
+ description:
+ - Will be C(routed) or C(unrouted).
+ returned: success
+ type: str
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.hrobot.plugins.module_utils.robot import (
+ ROBOT_DEFAULT_ARGUMENT_SPEC,
+)
+from ansible_collections.community.hrobot.plugins.module_utils.failover import (
+ get_failover,
+ set_failover,
+ get_failover_state,
+)
+
+
+def main():
+ argument_spec = dict(
+ failover_ip=dict(type='str', required=True),
+ state=dict(type='str', default='routed', choices=['routed', 'unrouted']),
+ value=dict(type='str'),
+ timeout=dict(type='int', default=180),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ required_if=(
+ ('state', 'routed', ['value']),
+ ),
+ )
+
+ failover_ip = module.params['failover_ip']
+ value = get_failover(module, failover_ip)
+ changed = False
+ before = get_failover_state(value)
+
+ if module.params['state'] == 'routed':
+ new_value = module.params['value']
+ else:
+ new_value = None
+
+ if value != new_value:
+ if module.check_mode:
+ value = new_value
+ changed = True
+ else:
+ value, changed = set_failover(module, failover_ip, new_value, timeout=module.params['timeout'])
+
+ after = get_failover_state(value)
+ module.exit_json(
+ changed=changed,
+ diff=dict(
+ before=before,
+ after=after,
+ ),
+ **after
+ )
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/failover_ip_info.py b/ansible_collections/community/hrobot/plugins/modules/failover_ip_info.py
new file mode 100644
index 000000000..b656b0499
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/failover_ip_info.py
@@ -0,0 +1,127 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: failover_ip_info
+short_description: Retrieve information on Hetzner's failover IPs
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Retrieve information on Hetzner's failover IPs.
+seealso:
+ - name: Failover IP documentation
+ description: Hetzner's documentation on failover IPs.
+ link: https://docs.hetzner.com/robot/dedicated-server/ip/failover/
+ - module: community.hrobot.failover_ip
+ description: Manage failover IPs.
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+ - community.hrobot.attributes.info_module
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+
+options:
+ failover_ip:
+ description: The failover IP address.
+ type: str
+ required: true
+'''
+
+EXAMPLES = r'''
+- name: Get value of failover IP 1.2.3.4
+ community.hrobot.failover_ip_info:
+ hetzner_user: foo
+ hetzner_password: bar
+ failover_ip: 1.2.3.4
+ value: 5.6.7.8
+ register: result
+
+- name: Print value of failover IP 1.2.3.4 in case it is routed
+ ansible.builtin.debug:
+ msg: "1.2.3.4 routes to {{ result.value }}"
+ when: result.state == 'routed'
+'''
+
+RETURN = r'''
+value:
+ description:
+ - The value of the failover IP.
+ - Will be C(none) if the IP is unrouted.
+ returned: success
+ type: str
+state:
+ description:
+ - Will be C(routed) or C(unrouted).
+ returned: success
+ type: str
+failover_ip:
+ description:
+ - The failover IP.
+ returned: success
+ type: str
+ sample: '1.2.3.4'
+failover_netmask:
+ description:
+ - The netmask for the failover IP.
+ returned: success
+ type: str
+ sample: '255.255.255.255'
+server_ip:
+ description:
+ - The main IP of the server this failover IP is associated to.
+ - This is I(not) the server the failover IP is routed to.
+ returned: success
+ type: str
+server_number:
+ description:
+ - The number of the server this failover IP is associated to.
+ - This is I(not) the server the failover IP is routed to.
+ returned: success
+ type: int
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.hrobot.plugins.module_utils.robot import (
+ ROBOT_DEFAULT_ARGUMENT_SPEC,
+)
+from ansible_collections.community.hrobot.plugins.module_utils.failover import (
+ get_failover_record,
+ get_failover_state,
+)
+
+
+def main():
+ argument_spec = dict(
+ failover_ip=dict(type='str', required=True),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ failover = get_failover_record(module, module.params['failover_ip'])
+ result = get_failover_state(failover['active_server_ip'])
+ result['failover_ip'] = failover['ip']
+ result['failover_netmask'] = failover['netmask']
+ result['server_ip'] = failover['server_ip']
+ result['server_number'] = failover['server_number']
+ result['changed'] = False
+ module.exit_json(**result)
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/firewall.py b/ansible_collections/community/hrobot/plugins/modules/firewall.py
new file mode 100644
index 000000000..2677a1186
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/firewall.py
@@ -0,0 +1,704 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: firewall
+short_description: Manage Hetzner's dedicated server firewall
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Manage Hetzner's dedicated server firewall.
+ - Note that idempotency check for TCP flags simply compares strings and doesn't
+ try to interpret the rules. This might change in the future.
+requirements:
+ - ipaddress
+seealso:
+ - name: Firewall documentation
+ description: Hetzner's documentation on the stateless firewall for dedicated servers
+ link: https://docs.hetzner.com/robot/dedicated-server/firewall/
+ - module: community.hrobot.firewall_info
+ description: Retrieve information on firewall configuration.
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+
+options:
+ server_ip:
+ description:
+ - The server's main IP address.
+ - Exactly one of I(server_ip) and I(server_number) must be specified.
+ - Note that Hetzner deprecated identifying the server's firewall by the server's main IP.
+ Using this option can thus stop working at any time in the future. Use I(server_number) instead.
+ type: str
+ server_number:
+ description:
+ - The server's number.
+ - Exactly one of I(server_ip) and I(server_number) must be specified.
+ type: int
+ version_added: 1.8.0
+ filter_ipv6:
+ description:
+ - Whether to filter IPv6 traffic as well.
+ - IPv4 traffic is always filtered, IPv6 traffic filtering needs to be explicitly enabled.
+ type: bool
+ version_added: 1.8.0
+ port:
+ description:
+ - Switch port of firewall.
+ type: str
+ choices: [ main, kvm ]
+ default: main
+ state:
+ description:
+ - Status of the firewall.
+ - Firewall is active if state is C(present), and disabled if state is C(absent).
+ type: str
+ default: present
+ choices: [ present, absent ]
+ allowlist_hos:
+ description:
+ - Whether Hetzner services have access.
+ type: bool
+ aliases:
+ - whitelist_hos
+ rules:
+ description:
+ - Firewall rules.
+ type: dict
+ suboptions:
+ input:
+ description:
+ - Input firewall rules.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description:
+ - Name of the firewall rule.
+ type: str
+ ip_version:
+ description:
+ - Internet protocol version.
+ - Leave away to filter both protocols. Note that in that case, none of I(dst_ip), I(src_ip), or I(protocol) can be specified.
+ type: str
+ dst_ip:
+ description:
+ - Destination IP address or subnet address.
+ - CIDR notation.
+ type: str
+ dst_port:
+ description:
+ - Destination port or port range.
+ type: str
+ src_ip:
+ description:
+ - Source IP address or subnet address.
+ - CIDR notation.
+ type: str
+ src_port:
+ description:
+ - Source port or port range.
+ type: str
+ protocol:
+ description:
+ - Protocol above IP layer.
+ type: str
+ tcp_flags:
+ description:
+ - TCP flags or logical combination of flags.
+ - Flags supported by Hetzner are C(syn), C(fin), C(rst), C(psh) and C(urg).
+ - They can be combined with C(|) (logical or) and C(&) (logical and).
+ - See L(the documentation,https://wiki.hetzner.de/index.php/Robot_Firewall/en#Parameter)
+ for more information.
+ type: str
+ action:
+ description:
+ - Action if rule matches.
+ required: true
+ type: str
+ choices: [ accept, discard ]
+ output:
+ description:
+ - Output firewall rules.
+ type: list
+ elements: dict
+ version_added: 1.8.0
+ suboptions:
+ name:
+ description:
+ - Name of the firewall rule.
+ type: str
+ ip_version:
+ description:
+ - Internet protocol version.
+ - Leave away to filter both protocols. Note that in that case, none of I(dst_ip), I(src_ip), or I(protocol) can be specified.
+ type: str
+ dst_ip:
+ description:
+ - Destination IP address or subnet address.
+ - CIDR notation.
+ type: str
+ dst_port:
+ description:
+ - Destination port or port range.
+ type: str
+ src_ip:
+ description:
+ - Source IP address or subnet address.
+ - CIDR notation.
+ type: str
+ src_port:
+ description:
+ - Source port or port range.
+ type: str
+ protocol:
+ description:
+ - Protocol above IP layer.
+ type: str
+ tcp_flags:
+ description:
+ - TCP flags or logical combination of flags.
+ - Flags supported by Hetzner are C(syn), C(fin), C(rst), C(psh) and C(urg).
+ - They can be combined with C(|) (logical or) and C(&) (logical and).
+ - See L(the documentation,https://wiki.hetzner.de/index.php/Robot_Firewall/en#Parameter)
+ for more information.
+ type: str
+ action:
+ description:
+ - Action if rule matches.
+ required: true
+ type: str
+ choices: [ accept, discard ]
+ update_timeout:
+ description:
+ - Timeout to use when configuring the firewall.
+ - Note that the API call returns before the firewall has been
+ successfully set up.
+ type: int
+ default: 30
+ wait_for_configured:
+ description:
+ - Whether to wait until the firewall has been successfully configured before
+ determining what to do, and before returning from the module.
+ - The API returns status C(in progress) when the firewall is currently
+ being configured. If this happens, the module will try again until
+ the status changes to C(active) or C(disabled).
+ - Please note that there is a request limit. If you have to do multiple
+ updates, it can be better to disable waiting, and regularly use
+ M(community.hrobot.firewall_info) to query status.
+ type: bool
+ default: true
+ wait_delay:
+ description:
+ - Delay to wait (in seconds) before checking again whether the firewall has
+ been configured.
+ type: int
+ default: 10
+ timeout:
+ description:
+ - Timeout (in seconds) for waiting for firewall to be configured.
+ type: int
+ default: 180
+'''
+
+EXAMPLES = r'''
+- name: Configure firewall for server with main IP 1.2.3.4
+ community.hrobot.firewall:
+ hetzner_user: foo
+ hetzner_password: bar
+ server_ip: 1.2.3.4
+ state: present
+ filter_ipv6: true
+ allowlist_hos: true
+ rules:
+ input:
+ - name: Allow ICMP protocol, so you can ping your server
+ ip_version: ipv4
+ protocol: icmp
+ action: accept
+ # Note that it is not possible to disable ICMP for IPv6
+ # (https://robot.hetzner.com/doc/webservice/en.html#post-firewall-server-id)
+ - name: Allow responses to incoming TCP connections
+ protocol: tcp
+ dst_port: '32768-65535'
+ tcp_flags: ack
+ action: accept
+ - name: Allow everything to ports 20-23 from 4.3.2.1/24 (IPv4 only)
+ ip_version: ipv4
+ src_ip: 4.3.2.1/24
+ dst_port: '20-23'
+ action: accept
+ - name: Allow everything to port 443
+ dst_port: '443'
+ action: accept
+ - name: Drop everything else
+ action: discard
+ output:
+ - name: Accept everything
+ action: accept
+ register: result
+
+- ansible.builtin.debug:
+ msg: "{{ result }}"
+'''
+
+RETURN = r'''
+firewall:
+ description:
+ - The firewall configuration.
+ type: dict
+ returned: success
+ contains:
+ port:
+ description:
+ - Switch port of firewall.
+ - C(main) or C(kvm).
+ type: str
+ sample: main
+ server_ip:
+ description:
+ - Server's main IP address.
+ type: str
+ sample: 1.2.3.4
+ server_number:
+ description:
+ - Hetzner's internal server number.
+ type: int
+ sample: 12345
+ status:
+ description:
+ - Status of the firewall.
+ - C(active) or C(disabled).
+ - Will be C(in process) if the firewall is currently updated, and
+ I(wait_for_configured) is set to C(false) or I(timeout) to a too small value.
+ type: str
+ sample: active
+ allowlist_hos:
+ description:
+ - Whether Hetzner services have access.
+ type: bool
+ sample: true
+ version_added: 1.2.0
+ whitelist_hos:
+ description:
+ - Whether Hetzner services have access.
+ - Old name of return value C(allowlist_hos), will be removed eventually.
+ type: bool
+ sample: true
+ rules:
+ description:
+ - Firewall rules.
+ type: dict
+ contains:
+ input:
+ description:
+ - Input firewall rules.
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - Name of the firewall rule.
+ type: str
+ sample: Allow HTTP access to server
+ ip_version:
+ description:
+ - Internet protocol version.
+ - No value means the rule applies both to IPv4 and IPv6.
+ type: str
+ sample: ipv4
+ dst_ip:
+ description:
+ - Destination IP address or subnet address.
+ - CIDR notation.
+ type: str
+ sample: 1.2.3.4/32
+ dst_port:
+ description:
+ - Destination port or port range.
+ type: str
+ sample: "443"
+ src_ip:
+ description:
+ - Source IP address or subnet address.
+ - CIDR notation.
+ type: str
+ sample: null
+ src_port:
+ description:
+ - Source port or port range.
+ type: str
+ sample: null
+ protocol:
+ description:
+ - Protocol above IP layer.
+ type: str
+ sample: tcp
+ tcp_flags:
+ description:
+ - TCP flags or logical combination of flags.
+ type: str
+ sample: null
+ action:
+ description:
+ - Action if rule matches.
+ - C(accept) or C(discard).
+ type: str
+ sample: accept
+ choices:
+ - accept
+ - discard
+ output:
+ description:
+ - Output firewall rules.
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - Name of the firewall rule.
+ type: str
+ sample: Allow HTTP access to server
+ ip_version:
+ description:
+ - Internet protocol version.
+ - No value means the rule applies both to IPv4 and IPv6.
+ type: str
+ sample: ~
+ dst_ip:
+ description:
+ - Destination IP address or subnet address.
+ - CIDR notation.
+ type: str
+ sample: 1.2.3.4/32
+ dst_port:
+ description:
+ - Destination port or port range.
+ type: str
+ sample: "443"
+ src_ip:
+ description:
+ - Source IP address or subnet address.
+ - CIDR notation.
+ type: str
+ sample: null
+ src_port:
+ description:
+ - Source port or port range.
+ type: str
+ sample: null
+ protocol:
+ description:
+ - Protocol above IP layer.
+ type: str
+ sample: tcp
+ tcp_flags:
+ description:
+ - TCP flags or logical combination of flags.
+ type: str
+ sample: null
+ action:
+ description:
+ - Action if rule matches.
+ - C(accept) or C(discard).
+ type: str
+ sample: accept
+ choices:
+ - accept
+ - discard
+'''
+
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible_collections.community.hrobot.plugins.module_utils.robot import (
+ ROBOT_DEFAULT_ARGUMENT_SPEC,
+ BASE_URL,
+ fetch_url_json,
+ fetch_url_json_with_retries,
+ CheckDoneTimeoutException,
+)
+from ansible.module_utils.six.moves.urllib.parse import urlencode
+from ansible.module_utils.common.text.converters import to_native, to_text
+
+try:
+ import ipaddress
+ HAS_IPADDRESS = True
+ IPADDRESS_IMP_ERR = None
+except ImportError as exc:
+ IPADDRESS_IMP_ERR = traceback.format_exc()
+ HAS_IPADDRESS = False
+
+
+RULE_OPTION_NAMES = [
+ 'name', 'ip_version', 'dst_ip', 'dst_port', 'src_ip', 'src_port',
+ 'protocol', 'tcp_flags', 'action',
+]
+
+RULES = ['input', 'output']
+
+
+def restrict_dict(dictionary, fields):
+ result = dict()
+ for k, v in dictionary.items():
+ if k in fields:
+ result[k] = v
+ return result
+
+
+def restrict_firewall_config(config):
+ result = restrict_dict(config, ['port', 'status', 'filter_ipv6', 'whitelist_hos'])
+ result['rules'] = dict()
+ for ruleset in RULES:
+ result['rules'][ruleset] = [
+ restrict_dict(rule, RULE_OPTION_NAMES)
+ for rule in config['rules'].get(ruleset) or []
+ ]
+ return result
+
+
+def update(before, after, params, name, param_name=None):
+ bv = before.get(name)
+ after[name] = bv
+ changed = False
+ pv = params[param_name or name]
+ if pv is not None:
+ changed = pv != bv
+ if changed:
+ after[name] = pv
+ return changed
+
+
+def normalize_ip(ip, ip_version):
+ if ip is None or ip_version is None:
+ return ip
+ if '/' in ip:
+ ip, range = ip.split('/')
+ else:
+ ip, range = ip, '' # pylint: disable=self-assigning-variable
+ ip_addr = to_native(ipaddress.ip_address(to_text(ip)).compressed)
+ if range == '':
+ range = '32' if ip_version.lower() == 'ipv4' else '128'
+ return ip_addr + '/' + range
+
+
+def update_rules(before, after, params, ruleset):
+ before_rules = before['rules'][ruleset]
+ after_rules = after['rules'][ruleset]
+ params_rules = params['rules'][ruleset]
+ changed = len(before_rules) != len(params_rules)
+ for no, rule in enumerate(params_rules):
+ rule['src_ip'] = normalize_ip(rule['src_ip'], rule['ip_version'])
+ rule['dst_ip'] = normalize_ip(rule['dst_ip'], rule['ip_version'])
+ if no < len(before_rules):
+ before_rule = before_rules[no]
+ before_rule['src_ip'] = normalize_ip(before_rule['src_ip'], before_rule['ip_version'])
+ before_rule['dst_ip'] = normalize_ip(before_rule['dst_ip'], before_rule['ip_version'])
+ if before_rule != rule:
+ changed = True
+ after_rules.append(rule)
+ return changed
+
+
+def encode_rule(output, rulename, input):
+ for i, rule in enumerate(input['rules'][rulename]):
+ for k, v in rule.items():
+ if v is not None:
+ output['rules[{0}][{1}][{2}]'.format(rulename, i, k)] = v
+
+
+def create_default_rules_object():
+ rules = dict()
+ for ruleset in RULES:
+ rules[ruleset] = []
+ return rules
+
+
+def fix_naming(firewall_result):
+ firewall_result = firewall_result.copy()
+ firewall_result['allowlist_hos'] = firewall_result.get('whitelist_hos', False)
+ return firewall_result
+
+
+def firewall_configured(result, error):
+ return result['firewall']['status'] != 'in process'
+
+
+def main():
+ argument_spec = dict(
+ server_ip=dict(type='str'),
+ server_number=dict(type='int'),
+ port=dict(type='str', default='main', choices=['main', 'kvm']),
+ filter_ipv6=dict(type='bool'),
+ state=dict(type='str', default='present', choices=['present', 'absent']),
+ allowlist_hos=dict(type='bool', aliases=['whitelist_hos']),
+ rules=dict(type='dict', options=dict(
+ input=dict(type='list', elements='dict', options=dict(
+ name=dict(type='str'),
+ ip_version=dict(type='str'),
+ dst_ip=dict(type='str'),
+ dst_port=dict(type='str'),
+ src_ip=dict(type='str'),
+ src_port=dict(type='str'),
+ protocol=dict(type='str'),
+ tcp_flags=dict(type='str'),
+ action=dict(type='str', required=True, choices=['accept', 'discard']),
+ ), required_by=dict(dst_ip=['ip_version'], src_ip=['ip_version'], protocol=['ip_version'])),
+ output=dict(type='list', elements='dict', options=dict(
+ name=dict(type='str'),
+ ip_version=dict(type='str'),
+ dst_ip=dict(type='str'),
+ dst_port=dict(type='str'),
+ src_ip=dict(type='str'),
+ src_port=dict(type='str'),
+ protocol=dict(type='str'),
+ tcp_flags=dict(type='str'),
+ action=dict(type='str', required=True, choices=['accept', 'discard']),
+ ), required_by=dict(dst_ip=['ip_version'], src_ip=['ip_version'], protocol=['ip_version'])),
+ )),
+ update_timeout=dict(type='int', default=30),
+ wait_for_configured=dict(type='bool', default=True),
+ wait_delay=dict(type='int', default=10),
+ timeout=dict(type='int', default=180),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ if not HAS_IPADDRESS:
+ module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMP_ERR)
+
+ # Sanitize input
+ module.params['status'] = 'active' if (module.params['state'] == 'present') else 'disabled'
+ if module.params['rules'] is None:
+ module.params['rules'] = {}
+ for chain in RULES:
+ if module.params['rules'].get(chain) is None:
+ module.params['rules'][chain] = []
+
+ server_id = module.params['server_ip'] or module.params['server_number']
+
+ # https://robot.your-server.de/doc/webservice/en.html#get-firewall-server-ip
+ url = "{0}/firewall/{1}".format(BASE_URL, server_id)
+ if module.params['wait_for_configured']:
+ try:
+ result, error = fetch_url_json_with_retries(
+ module,
+ url,
+ check_done_callback=firewall_configured,
+ check_done_delay=module.params['wait_delay'],
+ check_done_timeout=module.params['timeout'],
+ )
+ except CheckDoneTimeoutException as dummy:
+ module.fail_json(msg='Timeout while waiting for firewall to be configured.')
+ else:
+ result, error = fetch_url_json(module, url)
+ if not firewall_configured(result, error):
+ module.fail_json(msg='Firewall configuration cannot be read as it is not configured.')
+
+ full_before = result['firewall']
+ if not full_before.get('rules'):
+ full_before['rules'] = create_default_rules_object()
+ before = restrict_firewall_config(full_before)
+
+ # Build wanted (after) state and compare
+ after = dict(before)
+ changed = False
+ changed |= update(before, after, module.params, 'filter_ipv6')
+ changed |= update(before, after, module.params, 'port')
+ changed |= update(before, after, module.params, 'status')
+ changed |= update(before, after, module.params, 'whitelist_hos', 'allowlist_hos')
+ after['rules'] = create_default_rules_object()
+ if module.params['status'] == 'active':
+ for ruleset in RULES:
+ changed |= update_rules(before, after, module.params, ruleset)
+
+ # Update if different
+ construct_result = True
+ construct_status = None
+ if changed and not module.check_mode:
+ # https://robot.your-server.de/doc/webservice/en.html#post-firewall-server-ip
+ url = "{0}/firewall/{1}".format(BASE_URL, server_id)
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
+ data = dict(after)
+ data['filter_ipv6'] = str(data['filter_ipv6']).lower()
+ data['whitelist_hos'] = str(data['whitelist_hos']).lower()
+ del data['rules']
+ for ruleset in RULES:
+ encode_rule(data, ruleset, after)
+ result, error = fetch_url_json(
+ module,
+ url,
+ method='POST',
+ timeout=module.params['update_timeout'],
+ data=urlencode(data),
+ headers=headers,
+ )
+ if module.params['wait_for_configured'] and not firewall_configured(result, error):
+ try:
+ result, error = fetch_url_json_with_retries(
+ module,
+ url,
+ check_done_callback=firewall_configured,
+ check_done_delay=module.params['wait_delay'],
+ check_done_timeout=module.params['timeout'],
+ skip_first=True,
+ )
+ except CheckDoneTimeoutException as e:
+ result, error = e.result, e.error
+ module.warn('Timeout while waiting for firewall to be configured.')
+
+ full_after = result['firewall']
+ if not full_after.get('rules'):
+ full_after['rules'] = create_default_rules_object()
+ construct_status = full_after['status']
+ if construct_status != 'in process':
+ # Only use result if configuration is done, so that diff will be ok
+ after = restrict_firewall_config(full_after)
+ construct_result = False
+
+ if construct_result:
+ # Construct result (used for check mode, and configuration still in process)
+ full_after = dict(full_before)
+ for k, v in after.items():
+ if k != 'rules':
+ full_after[k] = after[k]
+ if construct_status is not None:
+ # We want 'in process' here
+ full_after['status'] = construct_status
+ full_after['rules'] = dict()
+ for ruleset in RULES:
+ full_after['rules'][ruleset] = after['rules'][ruleset]
+
+ module.exit_json(
+ changed=changed,
+ diff=dict(
+ before=fix_naming(before),
+ after=fix_naming(after),
+ ),
+ firewall=fix_naming(full_after),
+ )
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/firewall_info.py b/ansible_collections/community/hrobot/plugins/modules/firewall_info.py
new file mode 100644
index 000000000..49f98ab64
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/firewall_info.py
@@ -0,0 +1,318 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: firewall_info
+short_description: Manage Hetzner's dedicated server firewall
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Manage Hetzner's dedicated server firewall.
+seealso:
+ - name: Firewall documentation
+ description: Hetzner's documentation on the stateless firewall for dedicated servers
+ link: https://docs.hetzner.com/robot/dedicated-server/firewall/
+ - module: community.hrobot.firewall
+ description: Configure firewall.
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+ - community.hrobot.attributes.info_module
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+
+options:
+ server_ip:
+ description:
+ - The server's main IP address.
+ - Exactly one of I(server_ip) and I(server_number) must be specified.
+ - Note that Hetzner deprecated identifying the server's firewall by the server's main IP.
+ Using this option can thus stop working at any time in the future. Use I(server_number) instead.
+ type: str
+ server_number:
+ description:
+ - The server's number.
+ - Exactly one of I(server_ip) and I(server_number) must be specified.
+ type: int
+ version_added: 1.8.0
+ wait_for_configured:
+ description:
+ - Whether to wait until the firewall has been successfully configured before
+ returning from the module.
+ - The API returns status C(in progress) when the firewall is currently
+ being configured. If this happens, the module will try again until
+ the status changes to C(active) or C(disabled).
+ - Please note that there is a request limit. If you have to do multiple
+ updates, it can be better to disable waiting, and regularly use
+ M(community.hrobot.firewall_info) to query status.
+ type: bool
+ default: true
+ wait_delay:
+ description:
+ - Delay to wait (in seconds) before checking again whether the firewall has
+ been configured.
+ type: int
+ default: 10
+ timeout:
+ description:
+ - Timeout (in seconds) for waiting for firewall to be configured.
+ type: int
+ default: 180
+'''
+
+EXAMPLES = r'''
+- name: Get firewall configuration for server with main IP 1.2.3.4
+ community.hrobot.firewall_info:
+ hetzner_user: foo
+ hetzner_password: bar
+ server_ip: 1.2.3.4
+ register: result
+
+- ansible.builtin.debug:
+ msg: "{{ result.firewall }}"
+'''
+
+RETURN = r'''
+firewall:
+ description:
+ - The firewall configuration.
+ type: dict
+ returned: success
+ contains:
+ port:
+ description:
+ - Switch port of firewall.
+ - C(main) or C(kvm).
+ type: str
+ sample: main
+ filter_ipv6:
+ description:
+ - Whether the firewall rules apply to IPv6 as well or not.
+ type: bool
+ sample: false
+ server_ip:
+ description:
+ - Server's main IP address.
+ type: str
+ sample: 1.2.3.4
+ server_number:
+ description:
+ - Hetzner's internal server number.
+ type: int
+ sample: 12345
+ status:
+ description:
+ - Status of the firewall.
+ - C(active) or C(disabled).
+ - Will be C(in process) if the firewall is currently updated, and
+ I(wait_for_configured) is set to C(false) or I(timeout) to a too small value.
+ type: str
+ sample: active
+ allowlist_hos:
+ description:
+ - Whether Hetzner services have access.
+ type: bool
+ sample: true
+ version_added: 1.2.0
+ whitelist_hos:
+ description:
+ - Whether Hetzner services have access.
+ - Old name of return value C(allowlist_hos), will be removed eventually.
+ type: bool
+ sample: true
+ rules:
+ description:
+ - Firewall rules.
+ type: dict
+ contains:
+ input:
+ description:
+ - Input firewall rules.
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - Name of the firewall rule.
+ type: str
+ sample: Allow HTTP access to server
+ ip_version:
+ description:
+ - Internet protocol version.
+ - No value means the rule applies both to IPv4 and IPv6.
+ type: str
+ sample: ipv4
+ dst_ip:
+ description:
+ - Destination IP address or subnet address.
+ - CIDR notation.
+ type: str
+ sample: 1.2.3.4/32
+ dst_port:
+ description:
+ - Destination port or port range.
+ type: str
+ sample: "443"
+ src_ip:
+ description:
+ - Source IP address or subnet address.
+ - CIDR notation.
+ type: str
+ sample: null
+ src_port:
+ description:
+ - Source port or port range.
+ type: str
+ sample: null
+ protocol:
+ description:
+ - Protocol above IP layer.
+ type: str
+ sample: tcp
+ tcp_flags:
+ description:
+ - TCP flags or logical combination of flags.
+ type: str
+ sample: null
+ action:
+ description:
+ - Action if rule matches.
+ - C(accept) or C(discard).
+ type: str
+ sample: accept
+ choices:
+ - accept
+ - discard
+ output:
+ description:
+ - Output firewall rules.
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - Name of the firewall rule.
+ type: str
+ sample: Allow HTTP access to server
+ ip_version:
+ description:
+ - Internet protocol version.
+ - No value means the rule applies both to IPv4 and IPv6.
+ type: str
+ sample: ~
+ dst_ip:
+ description:
+ - Destination IP address or subnet address.
+ - CIDR notation.
+ type: str
+ sample: 1.2.3.4/32
+ dst_port:
+ description:
+ - Destination port or port range.
+ type: str
+ sample: "443"
+ src_ip:
+ description:
+ - Source IP address or subnet address.
+ - CIDR notation.
+ type: str
+ sample: null
+ src_port:
+ description:
+ - Source port or port range.
+ type: str
+ sample: null
+ protocol:
+ description:
+ - Protocol above IP layer.
+ type: str
+ sample: tcp
+ tcp_flags:
+ description:
+ - TCP flags or logical combination of flags.
+ type: str
+ sample: null
+ action:
+ description:
+ - Action if rule matches.
+ - C(accept) or C(discard).
+ type: str
+ sample: accept
+ choices:
+ - accept
+ - discard
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.hrobot.plugins.module_utils.robot import (
+ ROBOT_DEFAULT_ARGUMENT_SPEC,
+ BASE_URL,
+ fetch_url_json,
+ fetch_url_json_with_retries,
+ CheckDoneTimeoutException,
+)
+
+
+def firewall_configured(result, error):
+ return result['firewall']['status'] != 'in process'
+
+
+def main():
+ argument_spec = dict(
+ server_ip=dict(type='str'),
+ server_number=dict(type='int'),
+ wait_for_configured=dict(type='bool', default=True),
+ wait_delay=dict(type='int', default=10),
+ timeout=dict(type='int', default=180),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ server_id = module.params['server_ip'] or module.params['server_number']
+
+ # https://robot.your-server.de/doc/webservice/en.html#get-firewall-server-ip
+ url = "{0}/firewall/{1}".format(BASE_URL, server_id)
+ if module.params['wait_for_configured']:
+ try:
+ result, error = fetch_url_json_with_retries(
+ module,
+ url,
+ check_done_callback=firewall_configured,
+ check_done_delay=module.params['wait_delay'],
+ check_done_timeout=module.params['timeout'],
+ )
+ except CheckDoneTimeoutException as dummy:
+ module.fail_json(msg='Timeout while waiting for firewall to be configured.')
+ else:
+ result, error = fetch_url_json(module, url)
+
+ firewall = result['firewall']
+ firewall['allowlist_hos'] = firewall.get('whitelist_hos', False)
+ if not firewall.get('rules'):
+ firewall['rules'] = dict()
+ for ruleset in ['input']:
+ firewall['rules'][ruleset] = []
+
+ module.exit_json(
+ changed=False,
+ firewall=firewall,
+ )
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/reset.py b/ansible_collections/community/hrobot/plugins/modules/reset.py
new file mode 100644
index 000000000..d367936e0
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/reset.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: reset
+short_description: Reset a dedicated server
+version_added: 1.2.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Reset a dedicated server with a software or hardware reset, or by requesting a manual reset.
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+
+options:
+ server_number:
+ description:
+ - The server number of the server to reset.
+ type: int
+ required: true
+ reset_type:
+ description:
+ - How to reset the server.
+ - C(software) is a software reset. This should be similar to pressing Ctrl+Alt+Del on the keyboard.
+ - C(power) is a hardware reset similar to pressing the Power button. An ACPI signal is sent, and if the
+ server is configured correctly, this will trigger a regular shutdown.
+ - C(hardware) is a hardware reset similar to pressing the Restart button. The power is cycled for the server.
+ - C(manual) is a manual reset. This requests a technician to manually do the shutdown while looking at the
+ screen output. B(Be careful) and only use this when really necessary!
+ - Note that not every server supports every reset method!
+ type: str
+ required: true
+ choices:
+ - software
+ - hardware
+ - power
+ - manual
+'''
+
+EXAMPLES = r'''
+- name: Send ACPI signal to server to request controlled shutdown
+ community.hrobot.reset:
+ hetzner_user: foo
+ hetzner_password: bar
+ failover_ip: 1.2.3.4
+ state: power
+
+- name: Make sure that the server supports manual reset
+ community.hrobot.reset:
+ hetzner_user: foo
+ hetzner_password: bar
+ server_number: 1234
+ reset_type: manual
+ check_mode: true
+
+- name: Request a manual reset (by a technican)
+ community.hrobot.reset:
+ hetzner_user: foo
+ hetzner_password: bar
+ server_number: 1234
+ reset_type: manual
+'''
+
+RETURN = r''' # '''
+
+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,
+ fetch_url_json,
+)
+
+
+def main():
+ argument_spec = dict(
+ server_number=dict(type='int', required=True),
+ reset_type=dict(type='str', required=True, choices=['software', 'hardware', 'power', 'manual']),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ server_number = module.params['server_number']
+ reset_type = {
+ 'software': 'sw',
+ 'hardware': 'hw',
+ 'power': 'power',
+ 'manual': 'man',
+ }[module.params['reset_type']]
+
+ if module.check_mode:
+ url = "{0}/reset/{1}".format(BASE_URL, server_number)
+ result, error = fetch_url_json(module, url, accept_errors=['SERVER_NOT_FOUND', 'RESET_NOT_AVAILABLE'])
+ if not error and reset_type not in result['reset']['type']:
+ module.fail_json(msg='The chosen reset method is not supported for this server')
+ else:
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
+ data = dict(
+ type=reset_type,
+ )
+ url = "{0}/reset/{1}".format(BASE_URL, server_number)
+ result, error = fetch_url_json(
+ module,
+ url,
+ data=urlencode(data),
+ headers=headers,
+ method='POST',
+ accept_errors=['INVALID_INPUT', 'SERVER_NOT_FOUND', 'RESET_NOT_AVAILABLE', 'RESET_MANUAL_ACTIVE', 'RESET_FAILED'],
+ )
+ if error and error == 'INVALID_INPUT':
+ module.fail_json(msg='The chosen reset method is not supported for this server')
+ if error:
+ if error == 'SERVER_NOT_FOUND':
+ module.fail_json(msg='This server does not exist, or you do not have access rights for it')
+ if error == 'RESET_NOT_AVAILABLE':
+ module.fail_json(msg='The server has no reset option available')
+ if error == 'RESET_MANUAL_ACTIVE':
+ module.fail_json(msg='A manual reset is already running')
+ if error == 'RESET_FAILED':
+ module.fail_json(msg='The reset failed due to an internal error at Hetzner')
+ raise AssertionError('Unexpected error {0}'.format(error)) # pragma: no cover
+
+ module.exit_json(changed=True)
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/reverse_dns.py b/ansible_collections/community/hrobot/plugins/modules/reverse_dns.py
new file mode 100644
index 000000000..200489217
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/reverse_dns.py
@@ -0,0 +1,133 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: reverse_dns
+short_description: Set or remove reverse DNS entry for IP
+version_added: 1.2.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Allows to set, update or remove a reverse DNS entry for an IP address.
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+notes:
+ - For the main IPv4 address of a server, deleting it actually sets it to a default hostname like
+ C(static.X.Y.Z.W.clients.your-server.de). This substitution (delete is replaced by changing to
+ this value) is done automatically by the API and results in the module not being idempotent
+ in this case.
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+
+options:
+ ip:
+ description:
+ - The IP address to set or remove a reverse DNS entry for.
+ type: str
+ required: true
+ state:
+ description:
+ - Whether to set or update (C(present)) or delete (C(absent)) the reverse DNS entry for I(ip).
+ type: str
+ default: present
+ choices:
+ - present
+ - absent
+ value:
+ description:
+ - The reverse DNS entry for I(ip).
+ - Required if I(state=present).
+ type: str
+'''
+
+EXAMPLES = r'''
+- name: Set reverse DNS entry for 1.2.3.4
+ community.hrobot.reverse_dns:
+ hetzner_user: foo
+ hetzner_password: bar
+ ip: 1.2.3.4
+ value: foo.example.com
+
+- name: Remove reverse DNS entry for 2a01:f48:111:4221::1
+ community.hrobot.reverse_dns:
+ hetzner_user: foo
+ hetzner_password: bar
+ ip: 2a01:f48:111:4221::1
+ state: absent
+'''
+
+RETURN = r''' # '''
+
+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,
+ fetch_url_json,
+)
+
+
+def main():
+ argument_spec = dict(
+ ip=dict(type='str', required=True),
+ state=dict(type='str', choices=['present', 'absent'], default='present'),
+ value=dict(type='str'),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ required_if=[('state', 'present', ['value'])],
+ )
+
+ ip = module.params['ip']
+ state = module.params['state']
+ value = module.params['value']
+
+ url = "{0}/rdns/{1}".format(BASE_URL, ip)
+ result, error = fetch_url_json(module, url, accept_errors=['IP_NOT_FOUND', 'RDNS_NOT_FOUND'])
+ if error == 'RDNS_NOT_FOUND':
+ current = None
+ elif error:
+ if error == 'IP_NOT_FOUND':
+ module.fail_json(msg='The IP address was not found')
+ raise AssertionError('Unexpected error {0}'.format(error)) # pragma: no cover
+ else:
+ current = result['rdns']['ptr']
+
+ changed = False
+ expected = value if state == 'present' else None
+
+ if current != expected:
+ changed = True
+ if not module.check_mode:
+ if expected is None:
+ fetch_url_json(module, url, method='DELETE', allow_empty_result=True)
+ else:
+ headers = {'Content-type': 'application/x-www-form-urlencoded'}
+ data = {'ptr': expected}
+ fetch_url_json(module, url, data=urlencode(data), headers=headers, method='POST')
+
+ module.exit_json(changed=changed)
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/server.py b/ansible_collections/community/hrobot/plugins/modules/server.py
new file mode 100644
index 000000000..2a24986e3
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/server.py
@@ -0,0 +1,274 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: server
+short_description: Update server information
+version_added: 1.2.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Allows to update server information.
+ - Right now the API only supports updating the server's name.
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+
+options:
+ server_number:
+ description:
+ - The server number of the server to update.
+ type: int
+ required: true
+ server_name:
+ description:
+ - The server's name.
+ - If this option is not provided, it will not be adjusted.
+ type: str
+'''
+
+EXAMPLES = r'''
+- name: Set server's name to foo.example.com
+ community.hrobot.server:
+ hetzner_user: foo
+ hetzner_password: bar
+ server_number: 123
+ server_name: foo.example.com
+'''
+
+RETURN = r'''
+server:
+ description:
+ - Information on the server.
+ returned: success
+ type: dict
+ contains:
+ server_ip:
+ description:
+ - The server's main IP address.
+ type: str
+ sample: 123.123.123.123
+ returned: success
+ server_ipv6_net:
+ description:
+ - The server's main IPv6 network address.
+ type: str
+ sample: '2a01:f48:111:4221::'
+ returned: success
+ server_number:
+ description:
+ - The server's numeric ID.
+ type: int
+ sample: 321
+ returned: success
+ server_name:
+ description:
+ - The user-defined server's name.
+ type: str
+ sample: server1
+ returned: success
+ product:
+ description:
+ - The server product name.
+ type: str
+ sample: EQ 8
+ returned: success
+ dc:
+ description:
+ - The data center the server is located in.
+ type: str
+ sample: NBG1-DC1
+ returned: success
+ traffic:
+ description:
+ - Free traffic quota.
+ - C(unlimited) in case of unlimited traffic.
+ type: str
+ sample: 5 TB
+ returned: success
+ status:
+ description:
+ - Server status.
+ type: str
+ choices:
+ - ready
+ - in process
+ sample: ready
+ returned: success
+ cancelled:
+ description:
+ - Whether the server is cancelled.
+ type: bool
+ sample: false
+ returned: success
+ paid_until:
+ description:
+ - The date until the server has been paid.
+ type: str
+ sample: "2018-08-04"
+ returned: success
+ ip:
+ description:
+ - List of assigned single IP addresses.
+ type: list
+ elements: str
+ sample:
+ - 123.123.123.123
+ returned: success
+ subnet:
+ description:
+ - List of assigned subnets.
+ type: list
+ elements: dict
+ sample:
+ - ip: '2a01:4f8:111:4221::'
+ mask: 64
+ contains:
+ ip:
+ description:
+ - The first IP in the subnet.
+ type: str
+ sample: '2a01:4f8:111:4221::'
+ mask:
+ description:
+ - The masks bitlength.
+ type: str
+ sample: "64"
+ returned: success
+ reset:
+ description:
+ - Whether the server can be automatically reset.
+ type: bool
+ sample: true
+ returned: success
+ rescue:
+ description:
+ - Whether the rescue system is available.
+ type: bool
+ sample: false
+ returned: success
+ vnc:
+ description:
+ - Flag of VNC installation availability.
+ type: bool
+ sample: true
+ returned: success
+ windows:
+ description:
+ - Flag of Windows installation availability.
+ type: bool
+ sample: true
+ returned: success
+ plesk:
+ description:
+ - Flag of Plesk installation availability.
+ type: bool
+ sample: true
+ returned: success
+ cpanel:
+ description:
+ - Flag of cPanel installation availability.
+ type: bool
+ sample: true
+ returned: success
+ wol:
+ description:
+ - Flag of Wake On Lan availability.
+ type: bool
+ sample: true
+ returned: success
+ hot_swap:
+ description:
+ - Flag of Hot Swap availability.
+ type: bool
+ sample: true
+ returned: success
+ linked_storagebox:
+ description:
+ - Linked Storage Box ID.
+ type: int
+ sample: 12345
+ returned: success
+'''
+
+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,
+ fetch_url_json,
+)
+
+
+def main():
+ argument_spec = dict(
+ server_number=dict(type='int', required=True),
+ server_name=dict(type='str'),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ server_number = module.params['server_number']
+ server_name = module.params['server_name']
+
+ url = "{0}/server/{1}".format(BASE_URL, server_number)
+ server, error = fetch_url_json(module, url, accept_errors=['SERVER_NOT_FOUND'])
+ if error:
+ module.fail_json(msg='This server does not exist, or you do not have access rights for it')
+
+ result = {
+ 'changed': False,
+ 'server': server['server'],
+ }
+
+ update = {}
+ if server_name is not None:
+ if server_name != result['server']['server_name']:
+ update['server_name'] = server_name
+
+ if update:
+ result['changed'] = True
+ if module.check_mode:
+ result['server'].update(update)
+ else:
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
+ url = "{0}/server/{1}".format(BASE_URL, server_number)
+ server, error = fetch_url_json(
+ module,
+ url,
+ data=urlencode(update),
+ headers=headers,
+ method='POST',
+ accept_errors=['INVALID_INPUT'],
+ )
+ if error:
+ module.fail_json(msg='The values to update were invalid ({0})'.format(module.jsonify(update)))
+ result['server'] = server['server']
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/server_info.py b/ansible_collections/community/hrobot/plugins/modules/server_info.py
new file mode 100644
index 000000000..b3f6da11d
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/server_info.py
@@ -0,0 +1,283 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: server_info
+short_description: Query information on one or more servers
+version_added: 1.2.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Query information on one or more servers.
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+ - community.hrobot.attributes.info_module
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+
+options:
+ server_number:
+ description:
+ - Limit result list to server with this number.
+ type: int
+ server_name:
+ description:
+ - Limit result list to servers of this name.
+ type: str
+ full_info:
+ description:
+ - Whether to provide full information for every server.
+ - Setting this to C(true) requires one REST call per server,
+ which is slow and reduces your rate limit. Use with care.
+ - When I(server_number) is specified, this option is set to C(true).
+ type: bool
+ default: false
+'''
+
+EXAMPLES = r'''
+- name: Query a list of all servers
+ community.hrobot.server_info:
+ hetzner_user: foo
+ hetzner_password: bar
+ register: result
+
+- name: Query a specific server
+ community.hrobot.server_info:
+ hetzner_user: foo
+ hetzner_password: bar
+ server_number: 23
+ register: result
+
+- name: Output data on specific server
+ ansible.builtin.debug:
+ msg: "Server name: {{ result.servers[0].server_name }}"
+'''
+
+RETURN = r'''
+servers:
+ description:
+ - List of servers matching the provided options.
+ returned: success
+ type: list
+ elements: dict
+ contains:
+ server_ip:
+ description:
+ - The server's main IP address.
+ type: str
+ sample: 123.123.123.123
+ returned: success
+ server_ipv6_net:
+ description:
+ - The server's main IPv6 network address.
+ type: str
+ sample: '2a01:f48:111:4221::'
+ returned: success
+ server_number:
+ description:
+ - The server's numeric ID.
+ type: int
+ sample: 321
+ returned: success
+ server_name:
+ description:
+ - The user-defined server's name.
+ type: str
+ sample: server1
+ returned: success
+ product:
+ description:
+ - The server product name.
+ type: str
+ sample: EQ 8
+ returned: success
+ dc:
+ description:
+ - The data center the server is located in.
+ type: str
+ sample: NBG1-DC1
+ returned: success
+ traffic:
+ description:
+ - Free traffic quota.
+ - C(unlimited) in case of unlimited traffic.
+ type: str
+ sample: 5 TB
+ returned: success
+ status:
+ description:
+ - Server status.
+ type: str
+ choices:
+ - ready
+ - in process
+ sample: ready
+ returned: success
+ cancelled:
+ description:
+ - Whether the server is cancelled.
+ type: bool
+ sample: false
+ returned: success
+ paid_until:
+ description:
+ - The date until the server has been paid.
+ type: str
+ sample: "2018-08-04"
+ returned: success
+ ip:
+ description:
+ - List of assigned single IP addresses.
+ type: list
+ elements: str
+ sample:
+ - 123.123.123.123
+ returned: success
+ subnet:
+ description:
+ - List of assigned subnets.
+ type: list
+ elements: dict
+ sample:
+ - ip: '2a01:4f8:111:4221::'
+ mask: 64
+ contains:
+ ip:
+ description:
+ - The first IP in the subnet.
+ type: str
+ sample: '2a01:4f8:111:4221::'
+ mask:
+ description:
+ - The masks bitlength.
+ type: str
+ sample: "64"
+ returned: success
+ reset:
+ description:
+ - Whether the server can be automatically reset.
+ type: bool
+ sample: true
+ returned: when I(full_info=true)
+ rescue:
+ description:
+ - Whether the rescue system is available.
+ type: bool
+ sample: false
+ returned: when I(full_info=true)
+ vnc:
+ description:
+ - Flag of VNC installation availability.
+ type: bool
+ sample: true
+ returned: when I(full_info=true)
+ windows:
+ description:
+ - Flag of Windows installation availability.
+ type: bool
+ sample: true
+ returned: when I(full_info=true)
+ plesk:
+ description:
+ - Flag of Plesk installation availability.
+ type: bool
+ sample: true
+ returned: when I(full_info=true)
+ cpanel:
+ description:
+ - Flag of cPanel installation availability.
+ type: bool
+ sample: true
+ returned: when I(full_info=true)
+ wol:
+ description:
+ - Flag of Wake On Lan availability.
+ type: bool
+ sample: true
+ returned: when I(full_info=true)
+ hot_swap:
+ description:
+ - Flag of Hot Swap availability.
+ type: bool
+ sample: true
+ returned: when I(full_info=true)
+ linked_storagebox:
+ description:
+ - Linked Storage Box ID.
+ type: int
+ sample: 12345
+ returned: when I(full_info=true)
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.community.hrobot.plugins.module_utils.robot import (
+ BASE_URL,
+ ROBOT_DEFAULT_ARGUMENT_SPEC,
+ fetch_url_json,
+)
+
+
+def main():
+ argument_spec = dict(
+ server_number=dict(type='int'),
+ server_name=dict(type='str'),
+ full_info=dict(type='bool', default=False),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ server_number = module.params['server_number']
+ server_name = module.params['server_name']
+ full_info = module.params['full_info']
+
+ servers = []
+ if server_number is not None:
+ server_numbers = [server_number]
+ else:
+ url = "{0}/server".format(BASE_URL)
+ result, error = fetch_url_json(module, url, accept_errors=['SERVER_NOT_FOUND'])
+ server_numbers = []
+ if not error:
+ for entry in result:
+ if server_name is not None:
+ if entry['server']['server_name'] != server_name:
+ continue
+ if full_info:
+ server_numbers.append(entry['server']['server_number'])
+ else:
+ servers.append(entry['server'])
+
+ for server_number in server_numbers:
+ url = "{0}/server/{1}".format(BASE_URL, server_number)
+ result, error = fetch_url_json(module, url, accept_errors=['SERVER_NOT_FOUND'])
+ if not error:
+ if server_name is not None:
+ if result['server']['server_name'] != server_name:
+ continue
+ servers.append(result['server'])
+
+ module.exit_json(
+ changed=False,
+ servers=servers,
+ )
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/ssh_key.py b/ansible_collections/community/hrobot/plugins/modules/ssh_key.py
new file mode 100644
index 000000000..2353514b9
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/ssh_key.py
@@ -0,0 +1,245 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: ssh_key
+short_description: Add, remove or update SSH key
+version_added: 1.2.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Add, remove or update an SSH key stored in Hetzner's Robot.
+seealso:
+ - module: community.hrobot.ssh_key_info
+ description: Query information on SSH keys
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+
+attributes:
+ action_group:
+ version_added: 1.6.0
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+
+options:
+ state:
+ description:
+ - Whether to make sure a public SSH key is present or absent.
+ - C(present) makes sure that the SSH key is available, and
+ potentially updates names for existing SHS public keys.
+ - C(absent) makes sure that the SSH key is not available.
+ The fingerprint or public key data is used for matching the
+ key.
+ required: true
+ type: str
+ choices:
+ - present
+ - absent
+ name:
+ description:
+ - The public key's name.
+ - Required if I(state=present), and ignored if I(state=absent).
+ type: str
+ fingerprint:
+ description:
+ - The MD5 fingerprint of the public SSH key to remove.
+ - One of I(public_key) and I(fingerprint) are required if I(state=absent).
+ type: str
+ public_key:
+ description:
+ - The public key data in OpenSSH format.
+ - "Example: C(ssh-rsa AAAAB3NzaC1yc+...)"
+ - One of I(public_key) and I(fingerprint) are required if I(state=absent).
+ - Required if I(state=present).
+ type: str
+'''
+
+EXAMPLES = r'''
+- name: Add an SSH key
+ community.hrobot.ssh_key:
+ hetzner_user: foo
+ hetzner_password: bar
+ state: present
+ name: newKey
+ public_key: ssh-rsa AAAAB3NzaC1yc+...
+
+- name: Remove a SSH key by fingerprint
+ community.hrobot.ssh_key:
+ hetzner_user: foo
+ hetzner_password: bar
+ state: absent
+ fingerprint: cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99
+'''
+
+RETURN = r'''
+fingerprint:
+ description:
+ - The MD5 fingerprint of the key.
+ - This is the value used to reference the SSH public key, for example in the M(community.hrobot.boot) module.
+ returned: success
+ type: str
+ sample: cb:8b:ef:a7:fe:04:87:3f:e5:55:cd:12:e3:e8:9f:99
+'''
+
+import base64
+import binascii
+import re
+
+from ansible.module_utils.basic import AnsibleModule, AVAILABLE_HASH_ALGORITHMS
+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,
+ fetch_url_json,
+)
+
+
+class FingerprintError(Exception):
+ pass
+
+
+SPACE_RE = re.compile(' +')
+FINGERPRINT_PART = re.compile('^[0-9a-f]{2}$')
+
+
+def normalize_fingerprint(fingerprint, size=16):
+ if ':' in fingerprint:
+ fingerprint = fingerprint.split(':')
+ else:
+ fingerprint = [fingerprint[i:i + 2] for i in range(0, len(fingerprint), 2)]
+ if len(fingerprint) != size:
+ raise FingerprintError(
+ 'Fingerprint must consist of {0} 8-bit hex numbers: got {1} 8-bit hex numbers instead'.format(size, len(fingerprint)))
+ for i, part in enumerate(fingerprint):
+ new_part = part.lower()
+ if len(new_part) < 2:
+ new_part = '0{0}'.format(new_part)
+ if not FINGERPRINT_PART.match(new_part):
+ raise FingerprintError(
+ 'Fingerprint must consist of {0} 8-bit hex numbers: number {1} is invalid: "{2}"'.format(size, i + 1, part))
+ fingerprint[i] = new_part
+ return ':'.join(fingerprint)
+
+
+def extract_fingerprint(public_key, alg='md5', size=16):
+ try:
+ public_key = SPACE_RE.split(public_key.strip())[1]
+ except IndexError:
+ raise FingerprintError(
+ 'Error while extracting fingerprint from public key data: cannot split public key into at least two parts')
+ try:
+ public_key = base64.b64decode(public_key)
+ except (binascii.Error, TypeError) as exc:
+ raise FingerprintError(
+ 'Error while extracting fingerprint from public key data: {0}'.format(exc))
+ try:
+ algorithm = AVAILABLE_HASH_ALGORITHMS[alg]
+ except KeyError:
+ raise FingerprintError(
+ 'Hash algorithm {0} is not available. Possibly running in FIPS mode.'.format(alg.upper()))
+ digest = algorithm()
+ digest.update(public_key)
+ return normalize_fingerprint(digest.hexdigest(), size=size)
+
+
+def main():
+ argument_spec = dict(
+ state=dict(type='str', required=True, choices=['present', 'absent']),
+ name=dict(type='str'),
+ fingerprint=dict(type='str'),
+ public_key=dict(type='str'),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ mutually_exclusive=[
+ ('fingerprint', 'public_key'),
+ ],
+ required_if=[
+ ('state', 'present', ['name', 'public_key']),
+ ('state', 'absent', ['fingerprint', 'public_key'], True),
+ ],
+ )
+
+ state = module.params['state']
+ name = module.params['name']
+ fingerprint = module.params['fingerprint']
+ public_key = module.params['public_key']
+
+ try:
+ if fingerprint is not None:
+ fingerprint = normalize_fingerprint(fingerprint)
+ else:
+ fingerprint = extract_fingerprint(public_key)
+ except FingerprintError as exc:
+ module.fail_json(msg=to_native(exc))
+
+ url = "{0}/key/{1}".format(BASE_URL, fingerprint)
+
+ # Remove key
+ if state == 'absent':
+ if module.check_mode:
+ dummy, error = fetch_url_json(module, url, accept_errors=['NOT_FOUND'])
+ else:
+ dummy, error = fetch_url_json(module, url, accept_errors=['NOT_FOUND'], method='DELETE', allow_empty_result=True)
+ if error == 'NOT_FOUND':
+ changed = False
+ elif error is not None:
+ raise AssertionError('Unexpected error {0}'.format(error)) # pragma: no cover
+ else:
+ changed = True
+ module.exit_json(changed=changed, fingerprint=fingerprint)
+
+ # Make sure key is present
+ result, error = fetch_url_json(module, url, accept_errors=['NOT_FOUND'])
+ if error == 'NOT_FOUND':
+ changed = True
+ exists = False
+ elif error is not None:
+ raise AssertionError('Unexpected error {0}'.format(error)) # pragma: no cover
+ else:
+ exists = True
+ changed = False
+ # The only thing we can update is the name
+ if result['key'].get('name') != name:
+ changed = True
+
+ if changed and not module.check_mode:
+ data = {
+ 'name': name,
+ }
+ if not exists:
+ # Create key
+ data['data'] = ' '.join(SPACE_RE.split(public_key.strip())[:2])
+ url = "{0}/key".format(BASE_URL)
+ # Update or create key
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
+ result, dummy = fetch_url_json(
+ module,
+ url,
+ data=urlencode(data),
+ headers=headers,
+ method='POST',
+ )
+
+ module.exit_json(changed=changed, fingerprint=fingerprint)
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/ssh_key_info.py b/ansible_collections/community/hrobot/plugins/modules/ssh_key_info.py
new file mode 100644
index 000000000..69d4d8111
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/ssh_key_info.py
@@ -0,0 +1,113 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Felix Fontein <felix@fontein.de>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: ssh_key_info
+short_description: Query information on SSH keys
+version_added: 1.2.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - List information on all your SSH keys stored in Hetzner's Robot.
+seealso:
+ - module: community.hrobot.ssh_key
+ description: Add, remove or update SSH key
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+ - community.hrobot.attributes.info_module
+attributes:
+ action_group:
+ version_added: 1.6.0
+'''
+
+EXAMPLES = r'''
+- name: List all SSH keys
+ community.hrobot.ssh_key_info:
+ hetzner_user: foo
+ hetzner_password: bar
+ register: ssh_keys
+
+- name: Show how many keys were found
+ ansible.builtin.debug:
+ msg: "Found {{ ssh_keys.ssh_keys | length }} keys"
+'''
+
+RETURN = r'''
+ssh_keys:
+ description:
+ - The list of all SSH keys stored in Hetzner's Robot for your user.
+ returned: success
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - The key's name shown in the UI.
+ type: str
+ sample: key1
+ fingerprint:
+ description:
+ - The key's MD5 fingerprint.
+ type: str
+ sample: 56:29:99:a4:5d:ed:ac:95:c1:f5:88:82:90:5d:dd:10
+ type:
+ description:
+ - The key's algorithm type.
+ type: str
+ sample: ECDSA
+ size:
+ description:
+ - The key's size in bits.
+ type: int
+ sample: 521
+ data:
+ description:
+ - The key data in OpenSSH's format.
+ type: str
+ sample: ecdsa-sha2-nistp521 AAAAE2VjZHNh ...
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.community.hrobot.plugins.module_utils.robot import (
+ BASE_URL,
+ ROBOT_DEFAULT_ARGUMENT_SPEC,
+ fetch_url_json,
+)
+
+
+def main():
+ argument_spec = dict()
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ url = "{0}/key".format(BASE_URL)
+ result, error = fetch_url_json(module, url, accept_errors=['NOT_FOUND'])
+ if error == 'NOT_FOUND':
+ result = []
+ elif error is not None:
+ raise AssertionError('Unexpected error {0}'.format(error)) # pragma: no cover
+
+ keys = []
+ for key in result:
+ keys.append(key['key'])
+
+ module.exit_json(changed=False, ssh_keys=keys)
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover
diff --git a/ansible_collections/community/hrobot/plugins/modules/v_switch.py b/ansible_collections/community/hrobot/plugins/modules/v_switch.py
new file mode 100644
index 000000000..6035392a4
--- /dev/null
+++ b/ansible_collections/community/hrobot/plugins/modules/v_switch.py
@@ -0,0 +1,504 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Alexander Gil Casas <alexander.gilcasas@trustyou.net>
+# 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: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: v_switch
+short_description: Manage Hetzner's vSwitch
+version_added: 1.7.0
+author:
+ - Alexander Gil Casas (@pando85)
+description:
+ - Manage Hetzner's vSwitch.
+seealso:
+ - name: vSwitch documentation
+ description: Hetzner's documentation on vSwitch for connecting dedicated servers.
+ link: https://docs.hetzner.com/robot/dedicated-server/network/vswitch
+extends_documentation_fragment:
+ - community.hrobot.robot
+ - community.hrobot.attributes
+ - community.hrobot.attributes.actiongroup_robot
+
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+
+options:
+ vlan:
+ description:
+ - The vSwitch's VLAN ID.
+ - Range can be from 4000 to 4091.
+ - In order to identify a vSwitch both name and VLAN must match. If not, a new vSwitch will be created.
+ type: int
+ required: true
+ name:
+ description:
+ - The vSwitch's name.
+ - In order to identify a vSwitch both name and VLAN must match. If not, a new vSwitch will be created.
+ type: str
+ required: true
+ state:
+ description:
+ - State of the vSwitch.
+ - vSwitch is created if state is C(present), and deleted if state is C(absent).
+ - C(absent) just cancels the vSwitch at the end of the current day.
+ - When cancelling, you have to specify I(servers=[]) if you want to actively remove the servers in the vSwitch.
+ type: str
+ default: present
+ choices: [ present, absent ]
+ servers:
+ description:
+ - List of server identifiers (server's numeric ID or server's main IPv4 or IPv6).
+ - If servers is not specified, servers are not going to be deleted.
+ type: list
+ elements: str
+ wait:
+ description:
+ - Whether to wait until the vSwitch has been successfully configured before
+ determining what to do, and before returning from the module.
+ - The API returns status C(in process) when the vSwitch is currently
+ being set up in the servers. If this happens, the module will try again until
+ the status changes to C(ready) or server has been removed from vSwitch.
+ - Please note that if you disable wait while deleting and removing servers module
+ will fail with C(VSWITCH_IN_PROCESS) error.
+ type: bool
+ default: true
+ wait_delay:
+ description:
+ - Delay to wait (in seconds) before checking again whether the vSwitch servers has been configured.
+ type: int
+ default: 10
+ timeout:
+ description:
+ - Timeout (in seconds) for waiting for vSwitch servers to be configured.
+ type: int
+ default: 180
+'''
+
+EXAMPLES = r'''
+- name: Create vSwitch with VLAN 4010 and name foo
+ community.hrobot.v_switch:
+ hetzner_user: foo
+ hetzner_password: bar
+ vlan: 4010
+ name: foo
+
+- name: Create vSwitch with VLAN 4020 and name foo with two servers
+ community.hrobot.v_switch:
+ hetzner_user: foo
+ hetzner_password: bar
+ vlan: 4010
+ name: foo
+ servers:
+ - 123.123.123.123
+ - 154323
+'''
+
+RETURN = r'''
+v_switch:
+ description:
+ - Information on the vSwitch.
+ returned: success
+ type: dict
+ contains:
+ id:
+ description:
+ - The vSwitch's ID.
+ type: int
+ sample: 4321
+ returned: success
+ name:
+ description:
+ - The vSwitch's name.
+ type: str
+ sample: 'my vSwitch'
+ returned: success
+ vlan:
+ description:
+ - The vSwitch's VLAN ID.
+ type: int
+ sample: 4000
+ returned: success
+ cancelled:
+ description:
+ - Cancellation status.
+ type: bool
+ sample: false
+ returned: success
+ server:
+ description:
+ - The vSwitch's VLAN.
+ type: list
+ elements: dict
+ sample:
+ - server_ip: '123.123.123.123'
+ server_ipv6_net: '2a01:4f8:111:4221::'
+ server_number: 321
+ status: 'ready'
+ contains:
+ server_ip:
+ description:
+ - The server's main IP address.
+ type: str
+ sample: '123.123.123.123'
+ server_ipv6_net:
+ description:
+ - The server's main IPv6 network address.
+ type: str
+ sample: '2a01:f48:111:4221::'
+ server_number:
+ description:
+ - The server's numeric ID.
+ type: int
+ sample: 321
+ status:
+ description:
+ - Status of vSwitch for this server.
+ type: str
+ choices:
+ - ready
+ - in process
+ - failed
+ sample: 'ready'
+ returned: success
+ subnet:
+ description:
+ - List of assigned IP addresses.
+ type: list
+ elements: dict
+ sample:
+ - ip: '213.239.252.48'
+ mask: 29
+ gateway: '213.239.252.49'
+ contains:
+ ip:
+ description:
+ - IP address.
+ type: str
+ sample: '213.239.252.48'
+ mask:
+ description:
+ - Subnet mask in CIDR notation.
+ type: int
+ sample: 29
+ gateway:
+ description:
+ - Gateway of the subnet.
+ type: str
+ sample: '213.239.252.49'
+ returned: success
+ cloud_network:
+ description:
+ - List of assigned Cloud networks.
+ type: list
+ elements: dict
+ sample:
+ - id: 123
+ ip: '10.0.2.0'
+ mask: 24
+ gateway: '10.0.2.1'
+ contains:
+ id:
+ description:
+ - Cloud network ID.
+ type: int
+ sample: 123
+ ip:
+ description:
+ - IP address.
+ type: str
+ sample: '10.0.2.0'
+ mask:
+ description:
+ - Subnet mask in CIDR notation.
+ type: int
+ sample: 24
+ gateway:
+ description:
+ - Gateway.
+ type: str
+ sample: '10.0.2.1'
+ returned: success
+'''
+
+
+from datetime import datetime
+
+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,
+ get_x_www_form_urlenconded_dict_from_list,
+ fetch_url_json,
+ fetch_url_json_with_retries,
+ CheckDoneTimeoutException,
+)
+
+V_SWITCH_BASE_URL = '{0}/vswitch'.format(BASE_URL)
+
+
+def get_v_switch(module, id_, wait_condition=None):
+ url = '{0}/{1}'.format(V_SWITCH_BASE_URL, id_)
+ accept_errors = ['NOT_FOUND']
+ if wait_condition:
+ try:
+ result, error = fetch_url_json_with_retries(
+ module,
+ url,
+ check_done_callback=wait_condition,
+ check_done_delay=module.params['wait_delay'],
+ check_done_timeout=module.params['timeout'],
+ accept_errors=accept_errors,
+ )
+ except CheckDoneTimeoutException as dummy:
+ module.fail_json(msg='Timeout waiting vSwitch operation to finish')
+ else:
+ result, error = fetch_url_json(
+ module,
+ url,
+ accept_errors=accept_errors,
+ )
+
+ if error == 'NOT_FOUND':
+ module.fail_json(msg='vSwitch not found.')
+
+ return result
+
+
+def print_list(possible_list):
+ if isinstance(possible_list, list):
+ return [to_native(x) for x in possible_list]
+
+
+def create_v_switch(module):
+ headers = {'Content-type': 'application/x-www-form-urlencoded'}
+ data = {'name': module.params['name'], 'vlan': module.params['vlan']}
+ result, error = fetch_url_json(
+ module,
+ V_SWITCH_BASE_URL,
+ data=urlencode(data),
+ headers=headers,
+ method='POST',
+ accept_errors=['INVALID_INPUT', 'VSWITCH_LIMIT_REACHED'],
+ )
+ if error == 'INVALID_INPUT':
+ invalid_parameters = print_list(result['error']['invalid'])
+ module.fail_json(msg='vSwitch invalid parameter ({0})'.format(invalid_parameters))
+ elif error == 'VSWITCH_LIMIT_REACHED':
+ module.fail_json(msg='The maximum count of vSwitches is reached')
+
+ return result
+
+
+def delete_v_switch(module, id_):
+ url = '{0}/{1}'.format(V_SWITCH_BASE_URL, id_)
+ headers = {'Content-type': 'application/x-www-form-urlencoded'}
+ data = {'cancellation_date': datetime.now().strftime('%y-%m-%d')}
+ result, error = fetch_url_json(
+ module,
+ url,
+ data=urlencode(data),
+ headers=headers,
+ method='DELETE',
+ accept_errors=['INVALID_INPUT', 'NOT_FOUND', 'CONFLICT'],
+ allow_empty_result=True,
+ )
+ if error == 'INVALID_INPUT':
+ invalid_parameters = print_list(result['error']['invalid'])
+ module.fail_json(msg='vSwitch invalid parameter ({0})'.format(invalid_parameters))
+ elif error == 'NOT_FOUND':
+ module.fail_json(msg='vSwitch not found to delete')
+ elif error == 'CONFLICT':
+ module.fail_json(msg='The vSwitch is already cancelled')
+
+ return result
+
+
+def is_all_servers_ready(result, dummy):
+ return all(server['status'] == 'ready' for server in result['server'])
+
+
+def add_servers(module, id_, servers):
+ url = '{0}/{1}/server'.format(V_SWITCH_BASE_URL, id_)
+ headers = {'Content-type': 'application/x-www-form-urlencoded'}
+ data = get_x_www_form_urlenconded_dict_from_list('server', servers)
+ result, error = fetch_url_json(
+ module,
+ url,
+ data=urlencode(data),
+ headers=headers,
+ method='POST',
+ # TODO: missing NOT_FOUND, VSWITCH_NOT_AVAILABLE, VSWITCH_PER_SERVER_LIMIT_REACHED
+ accept_errors=[
+ 'INVALID_INPUT',
+ 'SERVER_NOT_FOUND',
+ 'VSWITCH_VLAN_NOT_UNIQUE',
+ 'VSWITCH_IN_PROCESS',
+ 'VSWITCH_SERVER_LIMIT_REACHED',
+ ],
+ allow_empty_result=True,
+ allowed_empty_result_status_codes=(201,),
+ )
+ if error == 'INVALID_INPUT':
+ invalid_parameters = print_list(result['error']['invalid'])
+ module.fail_json(msg='Invalid parameter adding server ({0})'.format(invalid_parameters))
+ elif error == 'SERVER_NOT_FOUND':
+ # information about which servers are failing is only there
+ module.fail_json(msg=result['error']['message'])
+ elif error == 'VSWITCH_VLAN_NOT_UNIQUE':
+ # information about which servers are failing is only there
+ module.fail_json(msg=result['error']['message'])
+ elif error == 'VSWITCH_IN_PROCESS':
+ module.fail_json(msg='There is a update running, therefore the vswitch can not be updated')
+ elif error == 'VSWITCH_SERVER_LIMIT_REACHED':
+ module.fail_json(msg='The maximum number of servers is reached for this vSwitch')
+
+ # TODO: add and delete with `wait=false`
+ wait_condition = is_all_servers_ready if module.params['wait'] else None
+ return get_v_switch(module, id_, wait_condition)
+
+
+def delete_servers(module, id_, servers):
+ url = '{0}/{1}/server'.format(V_SWITCH_BASE_URL, id_)
+ headers = {'Content-type': 'application/x-www-form-urlencoded'}
+ data = get_x_www_form_urlenconded_dict_from_list('server', servers)
+ result, error = fetch_url_json(
+ module,
+ url,
+ data=urlencode(data),
+ headers=headers,
+ method='DELETE',
+ # TODO: missing INVALID_INPUT, NOT_FOUND
+ accept_errors=['SERVER_NOT_FOUND', 'VSWITCH_IN_PROCESS'],
+ allow_empty_result=True,
+ )
+ if error == 'SERVER_NOT_FOUND':
+ # information about which servers are failing is only there
+ module.fail_json(msg=result['error']['message'])
+ elif error == 'VSWITCH_IN_PROCESS':
+ module.fail_json(msg='There is a update running, therefore the vswitch can not be updated')
+
+ wait_condition = is_all_servers_ready if module.params['wait'] else None
+ return get_v_switch(module, id_, wait_condition)
+
+
+def get_servers_to_delete(current_servers, desired_servers):
+ return [
+ server['server_ip']
+ for server in current_servers
+ if server['server_ip'] not in desired_servers
+ and server['server_ipv6_net'] not in desired_servers
+ and str(server['server_number']) not in desired_servers
+ ]
+
+
+def get_servers_to_add(current_servers, desired_servers):
+ current_ids = [str(server['server_number']) for server in current_servers]
+ current_ips = [server['server_ip'] for server in current_servers]
+ current_ipv6s = [server['server_ipv6_net'] for server in current_servers]
+
+ return [
+ server
+ for server in desired_servers
+ if server not in current_ips and server not in current_ids and server not in current_ipv6s
+ ]
+
+
+def set_desired_servers(module, id_):
+ v_switch = get_v_switch(module, id_)
+ changed = False
+
+ if module.params['servers'] is None:
+ return (v_switch, changed)
+
+ servers_to_delete = get_servers_to_delete(v_switch['server'], module.params['servers'])
+ if servers_to_delete:
+ if not module.check_mode:
+ v_switch = delete_servers(module, id_, servers_to_delete)
+ changed = True
+ servers_to_add = get_servers_to_add(v_switch['server'], module.params['servers'])
+ if servers_to_add:
+ if not module.check_mode:
+ v_switch = add_servers(module, id_, servers_to_add)
+ changed = True
+ return (v_switch, changed)
+
+
+def main():
+ argument_spec = dict(
+ vlan=dict(type='int', required=True),
+ name=dict(type='str', required=True),
+ state=dict(type='str', default='present', choices=['present', 'absent']),
+ servers=dict(type='list', elements='str'),
+ wait=dict(type='bool', default=True),
+ wait_delay=dict(type='int', default=10),
+ timeout=dict(type='int', default=180),
+ )
+ argument_spec.update(ROBOT_DEFAULT_ARGUMENT_SPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ v_switches, error = fetch_url_json(module, V_SWITCH_BASE_URL, accept_errors=['UNAUTHORIZED'])
+
+ if error:
+ module.fail_json(msg='Please check your current user and password configuration')
+
+ matched_v_switches = [
+ v
+ for v in v_switches
+ if v['name'] == module.params['name'] and v['vlan'] == module.params['vlan']
+ ]
+ non_cancelled_v_switches = [m for m in matched_v_switches if m['cancelled'] is False]
+ result = {'changed': False}
+
+ if len(non_cancelled_v_switches) > 1:
+ module.fail_json(
+ msg='Multiple vSwitches with same name and VLAN ID in non cancelled status. Clean it.'
+ )
+
+ elif len(non_cancelled_v_switches) == 1:
+ id_ = non_cancelled_v_switches[0]['id']
+ v_switch, changed = set_desired_servers(module, id_)
+ if changed:
+ result['changed'] = True
+
+ if module.params['state'] == 'present':
+ result['v_switch'] = v_switch
+ elif module.params['state'] == 'absent':
+ if not module.check_mode:
+ delete_v_switch(module, id_)
+ result['changed'] = True
+ else:
+ # not reachable
+ raise NotImplementedError
+ else:
+ if module.params['state'] == 'present':
+ result['changed'] = True
+ if not module.check_mode:
+ v_switch = create_v_switch(module)
+ if module.params['servers']:
+ result['v_switch'] = add_servers(module, v_switch['id'], module.params['servers'])
+ else:
+ result['v_switch'] = v_switch
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__': # pragma: no cover
+ main() # pragma: no cover