Source code for stepler.base

"""
----
Base
----
"""

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import types

import requests

__all__ = [
    'BaseApiClient',
    'BaseSteps',
    'Resource',
]


[docs]class BaseSteps(object): """Base steps-class. Steps-class aggregates steps over one resource. It means there are many inherited steps-classes for each resource. Note: Technically there are three types of steps: #. ``get``-steps Their goal to retrieve information about resource and return it. If information is unavailable, a step should raise exception. Rules to code: - step method name has prefix ``get_``, for ex: ``get_ips``, ``get_projects``, ``get_flavor``, ``get_network``. - step should return something in last code line, for ex: ``return projects``, ``return network``. It makes easier code review and debugging. - step can include argument ``check=True``. It's depends on context, for ex: step returns collection, but collection can be empty. And here ``check=True`` will help to check that collection has items. #. ``check``-steps Their goal to validate input resource and raise exception if it's invalid. Rules to code: - step method name has prefix ``check_``, for ex: ``check_image_presence``, ``check_project_bind_status``. - step should return nothing. - step should use code to raise exception: ``assert_that`` from hamcrest, ``wait``-methods which raise ``TimeoutExpired``, ``raise`` for explicit exception, another ``check_``-steps. .. warning:: We don't use python ``assert``, prefer ``hamcrest`` library. - step should include argument ``timeout=0`` to provide check during the time. #. ``change``-steps Their goal to change resource and check that changing is successful. Rules to code: - step method name starts with a verb denoting the action. - step has argument ``check=True``. - step makes action over resource before check. - inside ``if check:`` step should use code to raise exception: ``assert_that`` from hamcrest, ``wait``-methods which raise ``TimeoutExpired``, ``raise`` for explicit exception, or ``check_``-step. - inside ``if check:`` step should return nothing. Its section is for verification only. - if step should return something (for ex: ``return image``), it should be after ``if check:``. - if after resource changing need to update object with resource info, for ex: ``server.get()``, it must be inside ``if check:`` block (*see explanation below*) .. warning:: Explanation when in step it should to update object with resource information, for ex: when it should to call ``server.get()``. .. hint:: Keep in mind, that objects like ``server``, ``image``, ``volume``, ``flavor``, etc are not real resources. They are just objects with information about the state of resource at some point in time. And these info-objects should be updated from time to time. First of all its updating should NEVER call BEFORE resource changing. "Fresh" info-object must be passed to step. Updating of info-object must be executed inside the same step, where resource is changed. But there two variants for ex: .. code:: python # invalid variant def attach_floating_ip(self, server, floating_ip, check=True): self._client.add_floating_ip(server, floating_ip) server.get() if check: floating_ips = self.get_ips(server, 'floating').keys() assert floating_ip.ip in floating_ips .. code:: python # valid variant def attach_floating_ip(self, server, floating_ip, check=True): self._client.add_floating_ip(server, floating_ip) if check: server.get() floating_ips = self.get_ips(server, 'floating').keys() assert floating_ip.ip in floating_ips Let's understand why second variant is better that first. ``Change``-step consist of two stages: - change resource state - retrieve resource state for checking. And ``server.get()`` is resource state retrieving and must be a part of checking. This is more clearly for async steps. Let's see how previous invalid variant would look as async: .. code:: python # invalid async variant def attach_floating_ip_async(self, server, floating_ip, check=True): self._client.add_floating_ip(server, floating_ip) server.get() if check: self.check_status(server, 'active') floating_ips = self.get_ips(server, 'floating').keys() assert floating_ip.ip in floating_ips Here ``server.get()`` doesn't reflect actual state or resource, because resource is changed async. That why ``check_status`` is used to wait some stable resource state, and it uses ``server.get()`` inside itself. So we see double usage of ``server.get()`` and first its call absolutely useless. And in case, when step will be called with ``check=False``, call ``server.get()`` doesn't get actual resource state, because resource is changed async. It means that ``server.get()`` is useful for checking only, but otherwise it's useless. Common rules: - each step method has docstring, explaining its mission. See Also: **Why do we make steps with optional verification** ``check=True``? Sometimes (may be very rarely) we will need to make step without verification. In negative tests, for ex: try to create server without name and check that there is error. But more often we need positive verification by default to guarantee that step of a test was finished successfully and the test can go to another step. """ def __init__(self, client): """Constructor. Args: client (object): client for resources manipulation. """ self._client = client
class BaseApiClient(object): """Base API Client.""" def __init__(self, session): """Constructor. Args: session (object): keystone session. """ self._session = session def __getattr__(self, name): """Return new instance of API client. That mechanism allow to request method particulary, for ex.: ``cinder_client.volumes.create()``, but really method is defined as ``cinder_client.volumes_create()``. It allows to avoid redundant structure repetition and to provide full compatibility with python clients. Args: name (str): Name of attribute. Returns: BaseApiClient: New instance if attribute name matches existing attributes. Raises: AttributeError: If attribute name doesn't match exisiting attributes. """ matcher = name + '_' # particular methods prefix methods = {} # filter methods corresponding to particular request for attr, func in self.__class__.__dict__.items(): if attr.startswith(matcher): new_attr = attr.split(matcher, 1)[-1] methods[new_attr] = func if methods: client = self.__class__(self._session) # create clone of client # set particular methods to client for attr_name, func in methods.items(): method = types.MethodType(func, client, client.__class__) setattr(client, attr_name, method) return client else: return super(BaseApiClient, self).__getattribute__(name) @property def _auth_headers(self): """Get auth headers. Returns: dict: authentication headers. """ # TODO(schipiga): may be need to use native API return { # catch only token to avoid side effects 'X-Auth-Token': self._session.get_auth_headers()['X-Auth-Token'], } @property def _endpoint(self): """Get endpoint URL. Returns: str: endpoint URL. """ # TODO(schipiga): may be need to use native API raise NotImplemented def _head(self, url, headers=None, params=None, **kwgs): """HEAD request to API.""" headers = headers or {} headers.update(self._auth_headers) url = self._endpoint + url return requests.head(url, headers=headers, params=params, **kwgs) def _get(self, url, headers=None, params=None, **kwgs): """GET request to API.""" headers = headers or {} headers.update(self._auth_headers) url = self._endpoint + url return requests.get(url, headers=headers, params=params, **kwgs) def _put(self, url, headers=None, data=None, **kwgs): """PUT request to API.""" headers = headers or {} headers.update(self._auth_headers) url = self._endpoint + url return requests.put(url, headers=headers, data=data, **kwgs) def _post(self, url, headers=None, data=None, **kwgs): """POST request to API.""" headers = headers or {} headers.update(self._auth_headers) url = self._endpoint + url return requests.post(url, headers=headers, json=data, **kwgs) def _patch(self, url, headers=None, data=None, **kwgs): """PATCH request to API.""" headers = headers or {} headers.update(self._auth_headers) url = self._endpoint + url return requests.patch(url, headers=headers, data=data, **kwgs) def _delete(self, url, headers=None, **kwgs): """DELETE request to API.""" headers = headers or {} headers.update(self._auth_headers) url = self._endpoint + url return requests.delete(url, headers=headers, **kwgs) class Resource(object): """Unified resource with client API response. It's compatible with community python clients as far as it's used in tests. """ def __init__(self, client, info): """Constructor. Args: client (object): Client to call API. info (dict): Resource data structure. """ self._client = client self._info = info def get(self): """Refresh data from remote.""" if hasattr(self, 'uuid'): resource = self._client.get(self.uuid) else: resource = self._client.get(self.id) self._info.update(resource._info) def to_dict(self): """Get data as dict. Returns: dict: Resource data structure. """ return self._info def __getattr__(self, name): """Get data value as resource property. Args: name (str): Name of attribute. Returns: BaseApiClient: New instance if attribute name matches existing attributes. Raises: AttributeError: If attribute name doesn't match exisiting attributes. """ if name in self._info: return self._info[name] else: return super(Resource, self).__getattribute__(name)