Skip to content

Commit

Permalink
Add module_utils (#244)
Browse files Browse the repository at this point in the history
Signed-off-by: Alina Buzachis <[email protected]>
Co-authored-by: Abhijeet Kasurde <[email protected]>
  • Loading branch information
alinabuzachis and Akasurde authored Aug 12, 2024
1 parent 57099cd commit 3a75a16
Show file tree
Hide file tree
Showing 10 changed files with 642 additions and 4 deletions.
3 changes: 0 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ repos:
args: [
--fix,
--exit-non-zero-on-fix,
--ignore, E402,
]
- repo: https://github.com/PyCQA/flake8
rev: 7.1.1
Expand All @@ -55,8 +54,6 @@ repos:
- id: pylint
args:
- --output-format=colorized
- --disable=C0103,C0114,C0115,C0116,R0913,R1735,
- --max-line-length=120
additional_dependencies:
- aiobotocore
- aiohttp
Expand Down
Empty file.
47 changes: 47 additions & 0 deletions plugins/doc_fragments/eda_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-

# Copyright: Contributors to the Ansible project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)


class ModuleDocFragment:

AUTHS = """
options:
controller_host:
description:
- The URL of the EDA controller.
- If not set, the value of the C(CONTROLLER_URL) environment variable will be used.
required: true
type: str
version_added: '2.0.0'
controller_username:
description:
- Username used for authentication.
- If not set, the value of the C(CONTROLLER_USERNAME) environment variable will be used.
type: str
version_added: '2.0.0'
controller_password:
description:
- Password used for authentication.
- If not set, the value of the C(CONTROLLER_PASSWORD) environment variable will be used.
type: str
version_added: '2.0.0'
request_timeout:
description:
- Timeout in seconds for the connection with the EDA controller.
- If not set, the value of the C(CONTROLLER_TIMEOUT) environment variable will be used.
type: float
default: 10
version_added: '2.0.0'
validate_certs:
description:
- Whether to allow insecure connections to Ansible Automation Platform EDA
Controller instance.
- If C(no), SSL certificates will not be validated.
- This should only be used on personally controlled sites using self-signed certificates.
- If value not set, will try environment variable C(CONTROLLER_VERIFY_SSL)
default: True
type: bool
version_added: '2.0.0'
"""
Empty file.
34 changes: 34 additions & 0 deletions plugins/module_utils/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-

# Copyright: Contributors to the Ansible project
# 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.basic import env_fallback

AUTH_ARGSPEC = {
"controller_host": {
"fallback": (env_fallback, ["CONTROLLER_HOST"]),
"required": True,
},
"controller_username": {
"fallback": (env_fallback, ["CONTROLLER_USERNAME"]),
},
"controller_password": {
"fallback": (env_fallback, ["CONTROLLER_PASSWORD"]),
"no_log": True,
},
"validate_certs": {
"type": "bool",
"default": True,
"fallback": (env_fallback, ["CONTROLLER_VERIFY_SSL"]),
},
"request_timeout": {
"type": "float",
"default": 10.0,
"fallback": (env_fallback, ["CONTROLLER_REQUEST_TIMEOUT"]),
},
}
176 changes: 176 additions & 0 deletions plugins/module_utils/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-

# Copyright: Contributors to the Ansible project
# 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 urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urlparse

from ansible.module_utils.urls import Request

from .errors import AuthError, EDAHTTPError


class Response:
def __init__(self, status, data, headers=None):
self.status = status
self.data = data
# [('h1', 'v1'), ('H2', 'V2')] -> {'h1': 'v1', 'h2': 'V2'}
self.headers = (
dict((k.lower(), v) for k, v in dict(headers).items()) if headers else {}
)

self._json = None

@property
def json(self):
if self._json is None:
try:
self._json = json.loads(self.data)
except ValueError as value_exp:
raise EDAHTTPError(
f"Received invalid JSON response: {self.data}"
) from value_exp
return self._json


class Client:
def __init__(
self,
host,
username=None,
password=None,
timeout=None,
validate_certs=None,
):

if not (host or "").startswith(("https://", "http://")):
raise EDAHTTPError(
f"Invalid instance host value: '{host}'. "
"Value must start with 'https://' or 'http://'"
)

self.host = host
self.username = username
self.password = password
self.timeout = timeout
self.validate_certs = validate_certs

# Try to parse the hostname as a url
try:
self.url = urlparse(self.host)
# Store URL prefix for later use in build_url
self.url_prefix = self.url.path
except Exception as e:
raise EDAHTTPError(
f"Unable to parse eda_controller_host ({self.host}): {e}"
) from e

self.session = Request()

def _request(self, method, path, data=None, headers=None):
try:
raw_resp = self.session.open(
method,
path,
data=data,
url_password=self.password,
url_username=self.username,
headers=headers,
timeout=self.timeout,
validate_certs=self.validate_certs,
force_basic_auth=True,
)

except HTTPError as http_exp:
# Wrong username/password, or expired access token
if http_exp.code == 401:
raise AuthError(
f"Failed to authenticate with the instance: {http_exp.code} {http_exp.reason}"
) from http_exp
# Other HTTP error codes do not necessarily mean errors.
# This is for the caller to decide.
return Response(http_exp.code, http_exp.read(), http_exp.headers)
except URLError as url_exp:
raise EDAHTTPError(url_exp.reason) from url_exp

return Response(raw_resp.status, raw_resp.read(), raw_resp.headers)

def build_url(self, endpoint, query_params=None, identifier=None):
# Make sure we start with /api/vX
if not endpoint.startswith("/"):
endpoint = f"/{endpoint}"
prefix = self.url_prefix.rstrip("/")

if not endpoint.startswith(prefix + "/api/"):
endpoint = prefix + f"/api/eda/v1{endpoint}"
if not endpoint.endswith("/") and "?" not in endpoint:
endpoint = f"{endpoint}/"

# Update the URL path with the endpoint
url = self.url._replace(path=endpoint)

if query_params:
url = url._replace(query=urlencode(query_params))
if identifier:
url = url._replace(path=url.path + str(identifier) + "/")

return url

def request(self, method, endpoint, **kwargs):
# In case someone is calling us directly; make sure we were given a
# method, let's not just assume a GET
if not method:
raise EDAHTTPError("The HTTP method must be defined")

if method in ["POST"]:
url = self.build_url(endpoint)
elif method in ["DELETE", "PATCH", "PUT"]:
url = self.build_url(endpoint, identifier=kwargs.get("id"))
else:
url = self.build_url(endpoint, query_params=kwargs.get("data"))

# Extract the headers, this will be used in a couple of places
headers = kwargs.get("headers", {})

if method in ["POST", "PUT", "PATCH"]:
headers.setdefault("Content-Type", "application/json")
kwargs["headers"] = headers

data = (
None # Important, if content type is not JSON, this should not
# be dict type
)
if headers.get("Content-Type", "") == "application/json":
data = json.dumps(kwargs.get("data", {}))

return self._request(method, url.geturl(), data=data, headers=headers)

def get(self, path, **kwargs):
resp = self.request("GET", path, **kwargs)
if resp.status in (200, 404):
return resp
raise EDAHTTPError(f"HTTP error {resp.json}")

def post(self, path, **kwargs):
resp = self.request("POST", path, **kwargs)
if resp.status == 201:
return resp
raise EDAHTTPError(f"HTTP error {resp.json}")

def patch(self, path, **kwargs):
resp = self.request("PATCH", path, **kwargs)
if resp.status == 200:
return resp
raise EDAHTTPError(f"HTTP error {resp.json}")

def delete(self, path, **kwargs):
resp = self.request("DELETE", path, **kwargs)
if resp.status == 204:
return resp
raise EDAHTTPError(f"HTTP error {resp.json}")
Loading

0 comments on commit 3a75a16

Please sign in to comment.