Skip to content

Commit

Permalink
Move several functions to module pulp_smash.api
Browse files Browse the repository at this point in the history
Module `pulp_smash.api` contains several tools for working with Pulp's API.
Curiously, it imports two critically important function from module
`pulp_smash.utils`:

* `poll_spawned_tasks`
* `poll_task`

This is curious because module `pulp_smash.utils` contains a variety of
utilities, such as the `reset_pulp` function which resets the database and cache
on a Pulp server. Given this, shouldn't `pulp_smash.utils` build on the client
provided by `pulp_smash.api`, and not the other way around?

Fix this by moving these functions to module `pulp_smash.api`, and update the
docstring in module `pulp_smash.utils` to clearly state the relationship between
the two modules.

This commit doesn't affect test results. Both before and after this commit:

    =========  ==========  ==================
    Pulp Ver.  Num. Tests  Test Suite Results
    =========  ==========  ==================
    2.7        138         OK (skipped=19)
    dev (2.8)  127         OK (skipped=13)
    =========  ==========  ==================

Test results generated with:

    python -m unittest2 discover pulp_smash.tests

Fix #110. That issue asks for various utilities that make use of the API and ClI
to be moved out of `pulp_smash.utils`, which this commit does not do. However,
that request was made primarily becuase of a circular import issue, and this
solves that underlying issue.
  • Loading branch information
Ichimonji10 committed Feb 16, 2016
1 parent bf5cbe9 commit 45cadf4
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 79 deletions.
73 changes: 70 additions & 3 deletions pulp_smash/api.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
# coding=utf-8
"""Tools for working with Pulp's API."""
"""A client for working with Pulp's API.
Working with an API can require repetitive calls to perform actions like check
HTTP status codes. In addition, Pulp's API has specific quirks surrounding its
handling of href paths and HTTP 202 status codes. This module provides a
customizable client that makes it easier to work with the API in a safe and
concise manner.
"""
from __future__ import unicode_literals

import warnings
from time import sleep
try: # try Python 3 import first
from urllib.parse import urljoin
except ImportError: # pragma: no cover
from urlparse import urljoin # pylint:disable=C0411,E0401

import requests

from pulp_smash import utils
from pulp_smash import exceptions


_SENTINEL = object()
_TASK_END_STATES = ('canceled', 'error', 'finished', 'skipped', 'timed out')


def _check_http_202_content_type(response):
Expand Down Expand Up @@ -48,7 +57,7 @@ def _handle_202(server_config, response):
"""Check for an HTTP 202 response and handle it appropriately."""
if response.status_code == 202: # "Accepted"
_check_http_202_content_type(response)
tuple(utils.poll_spawned_tasks(server_config, response.json()))
tuple(poll_spawned_tasks(server_config, response.json()))


def echo_handler(server_config, response): # pylint:disable=unused-argument
Expand Down Expand Up @@ -250,3 +259,61 @@ def request(self, method, url, **kwargs):
self._cfg,
requests.request(method, **request_kwargs),
)


def poll_spawned_tasks(server_config, call_report):
"""Recursively wait for spawned tasks to complete. Yield response bodies.
Recursively wait for each of the spawned tasks listed in the given `call
report`_ to complete. For each task that completes, yield a response body
representing that task's final state.
:param server_config: A :class:`pulp_smash.config.ServerConfig` object.
:param call_report: A dict-like object with a `call report`_ structure.
:returns: A generator yielding task bodies.
:raises: Same as :meth:`poll_task`.
.. _call report:
http://pulp.readthedocs.org/en/latest/dev-guide/conventions/sync-v-async.html#call-report
"""
hrefs = (task['_href'] for task in call_report['spawned_tasks'])
for href in hrefs:
for final_task_state in poll_task(server_config, href):
yield final_task_state


def poll_task(server_config, href):
"""Wait for a task and its children to complete. Yield response bodies.
Poll the task at ``href``, waiting for the task to complete. When a
response is received indicating that the task is complete, yield that
response body and recursively poll each child task.
:param server_config: A :class:`pulp_smash.config.ServerConfig` object.
:param href: The path to a task you'd like to monitor recursively.
:returns: An generator yielding response bodies.
:raises pulp_smash.exceptions.TaskTimedOutError: If a task takes too
long to complete.
"""
poll_limit = 24 # 24 * 5s == 120s
poll_counter = 0
while True:
response = requests.get(
urljoin(server_config.base_url, href),
**server_config.get_requests_kwargs()
)
response.raise_for_status()
attrs = response.json()
if attrs['state'] in _TASK_END_STATES:
yield attrs
for spawned_task in attrs['spawned_tasks']:
yield poll_task(server_config, spawned_task['_href'])
break
poll_counter += 1
if poll_counter > poll_limit:
raise exceptions.TaskTimedOutError(
'Task {} is ongoing after {} polls.'.format(href, poll_limit)
)
# This approach is dumb, in that we don't account for time spent
# waiting for the Pulp server to respond to us.
sleep(5)
4 changes: 2 additions & 2 deletions pulp_smash/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class NoKnownServiceManagerError(Exception):
class TaskTimedOutError(Exception):
"""We timed out while polling a task and waiting for it to complete.
See :func:`pulp_smash.utils.poll_spawned_tasks` and
:func:`pulp_smash.utils.poll_task` for more information on how task polling
See :func:`pulp_smash.api.poll_spawned_tasks` and
:func:`pulp_smash.api.poll_task` for more information on how task polling
is handled.
"""
2 changes: 1 addition & 1 deletion pulp_smash/tests/ostree/api_v2/test_sync_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def _create_sync_repo(server_config, body):
{'override_config': {}},
)
response.raise_for_status()
tasks = tuple(utils.poll_spawned_tasks(server_config, response.json()))
tasks = tuple(api.poll_spawned_tasks(server_config, response.json()))
return repo['_href'], response, tasks


Expand Down
4 changes: 2 additions & 2 deletions pulp_smash/tests/puppet/api_v2/test_sync_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def setUpClass(cls):
report = client.post(urljoin(repo['_href'], 'actions/sync/'))
report.raise_for_status()
cls.reports.append(report)
for task in utils.poll_spawned_tasks(cls.cfg, report.json()):
for task in api.poll_spawned_tasks(cls.cfg, report.json()):
cls.tasks.append(task)

def test_status_code(self):
Expand Down Expand Up @@ -220,7 +220,7 @@ def setUpClass(cls):
client.response_handler = api.echo_handler
cls.report = client.post(urljoin(repo['_href'], 'actions/sync/'))
cls.report.raise_for_status()
cls.tasks = list(utils.poll_spawned_tasks(cls.cfg, cls.report.json()))
cls.tasks = list(api.poll_spawned_tasks(cls.cfg, cls.report.json()))

def test_status_code(self):
"""Assert the call to sync a repository returns an HTTP 202."""
Expand Down
4 changes: 2 additions & 2 deletions pulp_smash/tests/rpm/api_v2/test_sync_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def setUpClass(cls):
path = urljoin(repo['_href'], 'actions/sync/')
cls.report = client.post(path, {'override_config': {}})
cls.report.raise_for_status()
cls.tasks = tuple(utils.poll_spawned_tasks(cls.cfg, cls.report.json()))
cls.tasks = tuple(api.poll_spawned_tasks(cls.cfg, cls.report.json()))
client.response_handler = api.json_handler
cls.repo = client.get(repo['_href'])
cls.resources.add(repo['_href'])
Expand Down Expand Up @@ -231,7 +231,7 @@ def setUpClass(cls):
path = urljoin(repo['_href'], 'actions/sync/')
cls.report = client.post(path, {'override_config': {}})
cls.report.raise_for_status()
cls.tasks = tuple(utils.poll_spawned_tasks(cls.cfg, cls.report.json()))
cls.tasks = tuple(api.poll_spawned_tasks(cls.cfg, cls.report.json()))
cls.resources.add(repo['_href'])

def test_start_sync_code(self):
Expand Down
74 changes: 5 additions & 69 deletions pulp_smash/utils.py
Original file line number Diff line number Diff line change
@@ -1,86 +1,22 @@
# coding=utf-8
"""Utility functions for Pulp tests."""
"""Utility functions for Pulp tests.
This module may make use of :mod:`pulp_smash.api` and :mod:`pulp_smash.cli`,
but the reverse should not be done.
"""
from __future__ import unicode_literals

import uuid
from time import sleep
try: # try Python 3 import first
from urllib.parse import urljoin
except ImportError: # pragma: no cover
from urlparse import urljoin # pylint:disable=C0411,E0401

import requests

from pulp_smash import cli, exceptions
from pulp_smash.constants import PULP_SERVICES


_TASK_END_STATES = ('canceled', 'error', 'finished', 'skipped', 'timed out')


def uuid4():
"""Return a random UUID, as a unicode string."""
return type('')(uuid.uuid4())


def poll_spawned_tasks(server_config, call_report):
"""Recursively wait for spawned tasks to complete. Yield response bodies.
Recursively wait for each of the spawned tasks listed in the given `call
report`_ to complete. For each task that completes, yield a response body
representing that task's final state.
:param server_config: A :class:`pulp_smash.config.ServerConfig` object.
:param call_report: A dict-like object with a `call report`_ structure.
:returns: A generator yielding task bodies.
:raises: Same as :meth:`poll_task`.
.. _call report:
http://pulp.readthedocs.org/en/latest/dev-guide/conventions/sync-v-async.html#call-report
"""
hrefs = (task['_href'] for task in call_report['spawned_tasks'])
for href in hrefs:
for final_task_state in poll_task(server_config, href):
yield final_task_state


def poll_task(server_config, href):
"""Wait for a task and its children to complete. Yield response bodies.
Poll the task at ``href``, waiting for the task to complete. When a
response is received indicating that the task is complete, yield that
response body and recursively poll each child task.
:param server_config: A :class:`pulp_smash.config.ServerConfig` object.
:param href: The path to a task you'd like to monitor recursively.
:returns: An generator yielding response bodies.
:raises pulp_smash.exceptions.TaskTimedOutError: If a task takes too
long to complete.
"""
poll_limit = 24 # 24 * 5s == 120s
poll_counter = 0
while True:
response = requests.get(
urljoin(server_config.base_url, href),
**server_config.get_requests_kwargs()
)
response.raise_for_status()
attrs = response.json()
if attrs['state'] in _TASK_END_STATES:
yield attrs
for spawned_task in attrs['spawned_tasks']:
yield poll_task(server_config, spawned_task['_href'])
break
poll_counter += 1
if poll_counter > poll_limit:
raise exceptions.TaskTimedOutError(
'Task {} is ongoing after {} polls.'.format(href, poll_limit)
)
# This approach is dumb, in that we don't account for time spent
# waiting for the Pulp server to respond to us.
sleep(5)


# See design discussion at: https://github.com/PulpQE/pulp-smash/issues/31
def get_broker(server_config):
"""Build an object for managing the target system's AMQP broker.
Expand Down

0 comments on commit 45cadf4

Please sign in to comment.