Skip to content

Commit

Permalink
move some ansible-free code to apypie
Browse files Browse the repository at this point in the history
  • Loading branch information
evgeni committed Oct 28, 2024
1 parent 935f365 commit 3dd17c4
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 72 deletions.
166 changes: 166 additions & 0 deletions plugins/module_utils/_apypie.py
Original file line number Diff line number Diff line change
Expand Up @@ -921,3 +921,169 @@ def path_with_params(self, params=None):
else:
raise KeyError("missing param '{}' in parameters".format(param))
return result


import time


class ForemanApiException(Exception):

def __init__(self, msg, error=None):
self.msg = msg
self.error = error
return super(ForemanApiException, self).__init__()

def __repr__(self):
return str(self)

def __str__(self):
s = f'{self.__class__.__name__}: {self.msg}'
if self.error:
s += f' - {self.error}'
return s

@classmethod
def from_exception(cls, exc, msg):
error = None
if hasattr(exc, 'response'):
try:
response = exc.response.json()
if 'error' in response:
error = response['error']
else:
error = response
except Exception:
error = exc.response.text
return cls(msg=msg, error=error)


class ForemanApi(Api):

def __init__(self, **kwargs):
self.task_timeout = kwargs.pop('task_timeout', 60)
self.task_poll = 4
kwargs['api_version'] = 2
return super(ForemanApi, self).__init__(**kwargs)

def _resource(self, resource):
if resource not in self.resources:
raise Exception("The server doesn't know about {0}, is the right plugin installed?".format(resource))
return self.resource(resource)

def _resource_call(self, resource, *args, **kwargs):
return self._resource(resource).call(*args, **kwargs)

def _resource_prepare_params(self, resource, action, params):
api_action = self._resource(resource).action(action)
return api_action.prepare_params(params)

def resource_action(self, resource, action, params, options=None, data=None, files=None,
ignore_task_errors=False):
resource_payload = self._resource_prepare_params(resource, action, params)
if options is None:
options = {}
try:
result = self._resource_call(resource, action, resource_payload, options=options, data=data, files=files)
is_foreman_task = isinstance(result, dict) and 'action' in result and 'state' in result and 'started_at' in result
if is_foreman_task:
result = self.wait_for_task(result, ignore_errors=ignore_task_errors)
except Exception as e:
msg = 'Error while performing {0} on {1}: {2}'.format(
action, resource, str(e))
raise ForemanApiException.from_exception(e, msg) from e
return result

def wait_for_task(self, task, ignore_errors=False):
duration = self.task_timeout
while task['state'] not in ['paused', 'stopped']:
duration -= self.task_poll
if duration <= 0:
raise ForemanApiException(msg="Timeout waiting for Task {0}".format(task['id']))
time.sleep(self.task_poll)

resource_payload = self._resource_prepare_params('foreman_tasks', 'show', {'id': task['id']})
task = self._resource_call('foreman_tasks', 'show', resource_payload)
if not ignore_errors and task['result'] != 'success':
msg = 'Task {0}({1}) did not succeed. Task information: {2}'.format(task['action'], task['id'], task['humanized']['errors'])
raise ForemanApiException(msg=msg)
return task

def show(self, resource, resource_id, params=None):
"""
Execute the ``show`` action on an entity.
:param resource: Plural name of the api resource to show
:type resource: str
:param resource_id: The ID of the entity to show
:type resource_id: int
:param params: Lookup parameters (i.e. parent_id for nested entities)
:type params: Union[dict,None], optional
"""
payload = {'id': resource_id}
if params:
payload.update(params)
return self.resource_action(resource, 'show', payload)

def list(self, resource, search=None, params=None):
"""
Execute the ``index`` action on an resource.
:param resource: Plural name of the api resource to show
:type resource: str
:param search: Search string as accepted by the API to limit the results
:type search: str, optional
:param params: Lookup parameters (i.e. parent_id for nested entities)
:type params: Union[dict,None], optional
"""
PER_PAGE = 2 << 31
payload = {'per_page': PER_PAGE}
if search is not None:
payload['search'] = search
if params:
payload.update(params)

return self.resource_action(resource, 'index', payload)['results']

def create(self, resource, desired_entity, params=None):
"""
Create entity with given properties
:param resource: Plural name of the api resource to manipulate
:type resource: str
:param desired_entity: Desired properties of the entity
:type desired_entity: dict
:param params: Lookup parameters (i.e. parent_id for nested entities)
:type params: dict, optional
:return: The new current state of the entity
:rtype: dict
"""
payload = desired_entity.copy()
if params:
payload.update(params)
return self.resource_action(resource, 'create', payload)

def delete(self, resource, current_entity, params=None):
"""
Delete a given entity
:param resource: Plural name of the api resource to manipulate
:type resource: str
:param current_entity: Current properties of the entity
:type current_entity: dict
:param params: Lookup parameters (i.e. parent_id for nested entities)
:type params: dict, optional
:return: The new current state of the entity
:rtype: Union[dict,None]
"""
payload = {'id': current_entity['id']}
if params:
payload.update(params)
entity = self.resource_action(resource, 'destroy', payload)

# this is a workaround for https://projects.theforeman.org/issues/26937
if entity and isinstance(entity, dict) and 'error' in entity and 'message' in entity['error']:
raise ForemanApiException(msg=entity['error']['message'])

return None
86 changes: 14 additions & 72 deletions plugins/module_utils/foreman_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import os
import operator
import re
import time
import traceback

from contextlib import contextmanager
Expand Down Expand Up @@ -397,7 +396,6 @@ def __init__(self, **kwargs):
self.fail_json(msg="The server URL needs to be either HTTPS or HTTP!")

self.task_timeout = 60
self.task_poll = 4

self._thin_default = False
self.state = 'undefined'
Expand Down Expand Up @@ -608,12 +606,12 @@ def connect(self):
that are required by the module.
"""

self.foremanapi = apypie.Api(
self.foremanapi = apypie.ForemanApi(
uri=self._foremanapi_server_url,
username=to_bytes(self._foremanapi_username),
password=to_bytes(self._foremanapi_password),
api_version=2,
verify_ssl=self._foremanapi_validate_certs,
task_timeout=self.task_timeout,
)

_status = self.status()
Expand Down Expand Up @@ -651,18 +649,6 @@ def status(self):

return self.foremanapi.resource('home').call('status')

def _resource(self, resource):
if resource not in self.foremanapi.resources:
raise Exception("The server doesn't know about {0}, is the right plugin installed?".format(resource))
return self.foremanapi.resource(resource)

def _resource_call(self, resource, *args, **kwargs):
return self._resource(resource).call(*args, **kwargs)

def _resource_prepare_params(self, resource, action, params):
api_action = self._resource(resource).action(action)
return api_action.prepare_params(params)

@_exception2fail_json(msg='Failed to show resource: {0}')
def show_resource(self, resource, resource_id, params=None):
"""
Expand All @@ -676,16 +662,7 @@ def show_resource(self, resource, resource_id, params=None):
:type params: Union[dict,None], optional
"""

if params is None:
params = {}
else:
params = params.copy()

params['id'] = resource_id

params = self._resource_prepare_params(resource, 'show', params)

return self._resource_call(resource, 'show', params)
return self.foremanapi.show(resource, resource_id, params)

@_exception2fail_json(msg='Failed to list resource: {0}')
def list_resource(self, resource, search=None, params=None):
Expand All @@ -700,18 +677,7 @@ def list_resource(self, resource, search=None, params=None):
:type params: Union[dict,None], optional
"""

if params is None:
params = {}
else:
params = params.copy()

if search is not None:
params['search'] = search
params['per_page'] = PER_PAGE

params = self._resource_prepare_params(resource, 'index', params)

return self._resource_call(resource, 'index', params)['results']
return self.foremanapi.list(resource, search, params)

def find_resource(self, resource, search, params=None, failsafe=False, thin=None):
list_params = {}
Expand Down Expand Up @@ -1024,7 +990,7 @@ def _validate_supported_payload(self, resource, action, payload):
:return: The payload as it can be submitted to the API
:rtype: dict
"""
filtered_payload = self._resource_prepare_params(resource, action, payload)
filtered_payload = self.foremanapi._resource_prepare_params(resource, action, payload)
# On Python 2 dict.keys() is just a list, but we need a set here.
unsupported_parameters = set(payload.keys()) - set(_recursive_dict_keys(filtered_payload))
if unsupported_parameters:
Expand All @@ -1050,14 +1016,12 @@ def _create_entity(self, resource, desired_entity, params, foreman_spec):
"""
payload = _flatten_entity(desired_entity, foreman_spec)
self._validate_supported_payload(resource, 'create', payload)
self.set_changed()
if not self.check_mode:
if params:
payload.update(params)
return self.resource_action(resource, 'create', payload)
return self.foremanapi.create(resource, payload, params)
else:
fake_entity = desired_entity.copy()
fake_entity['id'] = -1
self.set_changed()
return fake_entity

def _update_entity(self, resource, desired_entity, current_entity, params, foreman_spec):
Expand Down Expand Up @@ -1183,29 +1147,18 @@ def _delete_entity(self, resource, current_entity, params):
:return: The new current state of the entity
:rtype: Union[dict,None]
"""
payload = {'id': current_entity['id']}
if params:
payload.update(params)
entity = self.resource_action(resource, 'destroy', payload)

# this is a workaround for https://projects.theforeman.org/issues/26937
if entity and isinstance(entity, dict) and 'error' in entity and 'message' in entity['error']:
self.fail_json(msg=entity['error']['message'])

return None
self.set_changed()
if not self.check_mode:
return self.foremanapi.delete(resource, current_entity, params)
else:
return None

def resource_action(self, resource, action, params, options=None, data=None, files=None,
ignore_check_mode=False, record_change=True, ignore_task_errors=False):
resource_payload = self._resource_prepare_params(resource, action, params)
if options is None:
options = {}
try:
result = None
if ignore_check_mode or not self.check_mode:
result = self._resource_call(resource, action, resource_payload, options=options, data=data, files=files)
is_foreman_task = isinstance(result, dict) and 'action' in result and 'state' in result and 'started_at' in result
if is_foreman_task:
result = self.wait_for_task(result, ignore_errors=ignore_task_errors)
result = self.foremanapi.resource_action(resource, action, params, options, data, files, ignore_task_errors)
except Exception as e:
msg = 'Error while performing {0} on {1}: {2}'.format(
action, resource, to_native(e))
Expand All @@ -1216,18 +1169,7 @@ def resource_action(self, resource, action, params, options=None, data=None, fil
return result

def wait_for_task(self, task, ignore_errors=False):
duration = self.task_timeout
while task['state'] not in ['paused', 'stopped']:
duration -= self.task_poll
if duration <= 0:
self.fail_json(msg="Timeout waiting for Task {0}".format(task['id']))
time.sleep(self.task_poll)

resource_payload = self._resource_prepare_params('foreman_tasks', 'show', {'id': task['id']})
task = self._resource_call('foreman_tasks', 'show', resource_payload)
if not ignore_errors and task['result'] != 'success':
self.fail_json(msg='Task {0}({1}) did not succeed. Task information: {2}'.format(task['action'], task['id'], task['humanized']['errors']))
return task
return self.foremanapi.wait_for_task(task, ignore_errors)

def fail_from_exception(self, exc, msg):
fail = {'msg': msg}
Expand Down

0 comments on commit 3dd17c4

Please sign in to comment.