Skip to content

Commit

Permalink
Refactoring and adding tests (#5)
Browse files Browse the repository at this point in the history
* Move generic open_url code for plugins to module_utils.

* Move failover code from module_utils/robot to module_utils/failover.

* Add boilerplate.

* Add tests for plugin_open_url_json.

* Make it work with all Python versions.

* Use community.internal_test_tools from ansible-collections/community.internal_test_tools#24

* Fix error instantiation.

* Add more complete test.

* The branch has been merged, but the collection hasn't been released yet.

* Forgot to remove path to my repo.
  • Loading branch information
felixfontein committed Dec 11, 2020
1 parent f737104 commit 954f033
Show file tree
Hide file tree
Showing 9 changed files with 485 additions and 256 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ansible-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ jobs:
# OPTIONAL If your unit test requires Python libraries from other collections
# Install them like this
- name: Install collection dependencies
run: ansible-galaxy collection install community.internal_test_tools -p .
#run: ansible-galaxy collection install community.internal_test_tools -p .
run: git clone https://github.com/ansible-collections/community.internal_test_tools.git ansible_collections/community/internal_test_tools

# Run the unit tests
- name: Run unit test
Expand Down
24 changes: 6 additions & 18 deletions plugins/inventory/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,13 @@
"""

from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.module_utils.urls import open_url
from ansible.utils.display import Display
from ansible.errors import AnsibleError

import json

from ansible_collections.community.hrobot.plugins.module_utils.robot import (
BASE_URL
BASE_URL,
PluginException,
plugin_open_url_json,
)

display = Display()
Expand Down Expand Up @@ -169,18 +168,7 @@ def filter(self, server, filters):
return matched

def get_servers(self):
user = self.get_option('hetzner_user')
password = self.get_option('hetzner_password')
try:
response = open_url(
"{0}/server".format(BASE_URL),
url_username=user,
url_password=password,
headers={"Content-type": "application/x-www-form-urlencoded"}
)
servers = json.loads(response.read())
return servers
except ValueError:
raise AnsibleError(msg='Cannot decode content retrieved from to Hetzner Robot server endpoint')
except Exception as e:
raise AnsibleError(msg='Failed request to Hetzner Robot server endpoint')
return plugin_open_url_json(self, '{0}/server'.format(BASE_URL))[0]
except PluginException as e:
raise AnsibleError(e.error_message)
91 changes: 91 additions & 0 deletions plugins/module_utils/failover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-

# Copyright (c), Felix Fontein <felix@fontein.de>, 2019
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


import json

from ansible.module_utils.six.moves.urllib.parse import urlencode

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


def get_failover_record(module, ip):
'''
Get information record of failover IP.
See https://robot.your-server.de/doc/webservice/en.html#get-failover-failover-ip
'''
url = "{0}/failover/{1}".format(BASE_URL, ip)
result, error = fetch_url_json(module, url)
if 'failover' not in result:
module.fail_json(msg='Cannot interpret result: {0}'.format(json.dumps(result, sort_keys=True)))
return result['failover']


def get_failover(module, ip):
'''
Get current routing target of failover IP.
The value ``None`` represents unrouted.
See https://robot.your-server.de/doc/webservice/en.html#get-failover-failover-ip
'''
return get_failover_record(module, ip)['active_server_ip']


def set_failover(module, ip, value, timeout=180):
'''
Set current routing target of failover IP.
Return a pair ``(value, changed)``. The value ``None`` for ``value`` represents unrouted.
See https://robot.your-server.de/doc/webservice/en.html#post-failover-failover-ip
and https://robot.your-server.de/doc/webservice/en.html#delete-failover-failover-ip
'''
url = "{0}/failover/{1}".format(BASE_URL, ip)
if value is None:
result, error = fetch_url_json(
module,
url,
method='DELETE',
timeout=timeout,
accept_errors=['FAILOVER_ALREADY_ROUTED']
)
else:
headers = {"Content-type": "application/x-www-form-urlencoded"}
data = dict(
active_server_ip=value,
)
result, error = fetch_url_json(
module,
url,
method='POST',
timeout=timeout,
data=urlencode(data),
headers=headers,
accept_errors=['FAILOVER_ALREADY_ROUTED']
)
if error is not None:
return value, False
else:
return result['failover']['active_server_ip'], True


def get_failover_state(value):
'''
Create result dictionary for failover IP's value.
The value ``None`` represents unrouted.
'''
return dict(
value=value,
state='routed' if value else 'unrouted'
)
138 changes: 52 additions & 86 deletions plugins/module_utils/robot.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
# -*- coding: utf-8 -*-

# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Copyright (c), Felix Fontein <felix@fontein.de>, 2019
#
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.urls import fetch_url, open_url
from ansible.module_utils.six.moves.urllib.error import HTTPError

import json
import time
Expand All @@ -30,6 +23,56 @@
BASE_URL = "https://robot-ws.your-server.de"


class PluginException(Exception):
def __init__(self, message):
super(PluginException, self).__init__(message)
self.error_message = message


def plugin_open_url_json(plugin, url, method='GET', timeout=10, data=None, headers=None, accept_errors=None):
'''
Make general request to Hetzner's JSON robot API.
'''
user = plugin.get_option('hetzner_user')
password = plugin.get_option('hetzner_password')
try:
response = open_url(
url,
url_username=user,
url_password=password,
data=data,
headers=headers,
method=method,
timeout=timeout,
)
content = response.read()
except HTTPError as e:
try:
content = e.read()
except AttributeError:
content = b''
except Exception:
raise PluginException('Failed request to Hetzner Robot server endpoint')

if not content:
raise PluginException('Cannot retrieve content from {0}'.format(url))

try:
result = json.loads(content.decode('utf-8'))
if 'error' in result:
if accept_errors:
if result['error']['code'] in accept_errors:
return result, result['error']['code']
raise PluginException('Request failed: {0} {1} ({2})'.format(
result['error']['status'],
result['error']['code'],
result['error']['message']
))
return result, None
except ValueError:
raise PluginException('Cannot decode content retrieved from {0}'.format(url))


def fetch_url_json(module, url, method='GET', timeout=10, data=None, headers=None, accept_errors=None):
'''
Make general request to Hetzner's JSON robot API.
Expand Down Expand Up @@ -93,80 +136,3 @@ def fetch_url_json_with_retries(module, url, check_done_callback, check_done_del
return result, error
if left_time < check_done_delay:
raise CheckDoneTimeoutException(result, error)


# #####################################################################################
# ## FAILOVER IP ######################################################################

def get_failover_record(module, ip):
'''
Get information record of failover IP.
See https://robot.your-server.de/doc/webservice/en.html#get-failover-failover-ip
'''
url = "{0}/failover/{1}".format(BASE_URL, ip)
result, error = fetch_url_json(module, url)
if 'failover' not in result:
module.fail_json(msg='Cannot interpret result: {0}'.format(json.dumps(result, sort_keys=True)))
return result['failover']


def get_failover(module, ip):
'''
Get current routing target of failover IP.
The value ``None`` represents unrouted.
See https://robot.your-server.de/doc/webservice/en.html#get-failover-failover-ip
'''
return get_failover_record(module, ip)['active_server_ip']


def set_failover(module, ip, value, timeout=180):
'''
Set current routing target of failover IP.
Return a pair ``(value, changed)``. The value ``None`` for ``value`` represents unrouted.
See https://robot.your-server.de/doc/webservice/en.html#post-failover-failover-ip
and https://robot.your-server.de/doc/webservice/en.html#delete-failover-failover-ip
'''
url = "{0}/failover/{1}".format(BASE_URL, ip)
if value is None:
result, error = fetch_url_json(
module,
url,
method='DELETE',
timeout=timeout,
accept_errors=['FAILOVER_ALREADY_ROUTED']
)
else:
headers = {"Content-type": "application/x-www-form-urlencoded"}
data = dict(
active_server_ip=value,
)
result, error = fetch_url_json(
module,
url,
method='POST',
timeout=timeout,
data=urlencode(data),
headers=headers,
accept_errors=['FAILOVER_ALREADY_ROUTED']
)
if error is not None:
return value, False
else:
return result['failover']['active_server_ip'], True


def get_failover_state(value):
'''
Create result dictionary for failover IP's value.
The value ``None`` represents unrouted.
'''
return dict(
value=value,
state='routed' if value else 'unrouted'
)
2 changes: 2 additions & 0 deletions plugins/modules/failover_ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
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,
Expand Down
2 changes: 2 additions & 0 deletions plugins/modules/failover_ip_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@
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,
)
Expand Down

0 comments on commit 954f033

Please sign in to comment.