Source code for decapodlib.client

# -*- coding: utf-8 -*-
# Copyright (c) 2016 Mirantis Inc.
#
# 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.
"""This module contains implementation of RPC client for Decapod API.

Decapod client :py:class:`Client` is a simple RPC client and thin wrapper
for the `requests <http://docs.python-requests.org/en/master/>`_ library
which allows end user to work with remote API without worrying about
connections and endpoints.

RPC client itself manages authorization (therefore you have to supply
it with user/password pair on initialization) so there is no need in
explicit session objects but if you do not like that way, you may always
relogin explicitly.

Usage example:

.. code-block:: python

    client = Client(url="http://localhost", login="root", password="root")

This will initialize new client. Initialization does not imply immediate login,
login would be occured thread-safely on the first real method execution.

.. code-block:: python

    users = client.get_users()

This will return end user a list with active users in Decapod.

.. code-block:: json

    [
        {
            "data": {
                "email": "noreply@example.com",
                "full_name": "Root User",
                "login": "root",
                "role_id": "37fb532f-2620-4e0d-80e6-b68ed6988a6d"
            },
            "id": "6567c2ab-54cc-40b7-a811-6147a3f3ea83",
            "initiator_id": null,
            "model": "user",
            "time_deleted": 0,
            "time_updated": 1478865388,
            "version": 1
        }
    ]

Incoming JSON will be parsed. If it is not possible,
:py:exc:`decapodlib.exceptions.DecapodError` will be raised.
"""


from __future__ import absolute_import
from __future__ import unicode_literals

import abc
import inspect
import logging
import socket
import warnings

import pkg_resources
import requests
import requests.adapters
import six

from decapodlib import auth
from decapodlib import exceptions

try:
    import simplejson as json
except ImportError:
    import json


LOG = logging.getLogger(__name__)
"""Logger."""

try:
    VERSION = pkg_resources.get_distribution("decapodlib").version
except pkg_resources.DistributionNotFound as exc:
    warnings.warn("Module is imported outside of distribution.", ImportWarning)
    VERSION = "unknown"

__all__ = "VERSION", "Client", "V1Client"


def json_dumps(data):
    """Makes compact JSON dumps.

    :param data: Data which should be encoded to JSON.
    :type data: Any data, suitable for :py:func:`json.dumps`
    :return: Data, encoded to JSON.
    :rtype: str
    :raises ValueError: if data cannot be encoded to JSON.
    """

    return json.dumps(data, separators=(",", ":"))


def make_query_params(**request_params):
    """Makes query string parameters for request.

    The reason to have this function is to exclude parameters which value
    is ``None``.

    :param request_params: Keyword arguments to be used as GET query
        params later.
    :return: Parameters to be encoded for GET query.
    :rtype: dict
    """

    params = {}
    for key, value in six.iteritems(request_params):
        if value is not None:
            params[key] = value

    return params


def json_response(func):
    """Decorator which parses :py:class:`requests.Response` and
    returns unpacked JSON. If ``Content-Type`` of response is not
    ``application/json``, then it returns text.

    :return: Data of :py:class:`requests.Response` from decorated
        function.
    :raises decapodlib.exceptions.DecapodAPIError: if decoding is not possible
        or response status code is not ``200``.
    """

    @six.wraps(func)
    def decorator(*args, **kwargs):
        raw_response = kwargs.pop("raw_response", False)
        response = func(*args, **kwargs)
        if raw_response:
            return response

        if isinstance(response, dict):
            return response

        if response.ok:
            content_type = response.headers.get("Content-Type")
            content_type = content_type or "application/json"
            if content_type == "application/json":
                return response.json()
            return response.text

        raise exceptions.DecapodAPIError(response)

    return decorator


def inject_timeout(func):
    """Decorator which injects ``timeout`` parameter into request.

    On client initiation, default timeout is set. This timeout will be
    injected into any request if no explicit parameter is set.

    :return: Value of decorated function.
    """

    @six.wraps(func)
    def decorator(self, *args, **kwargs):
        kwargs.setdefault("timeout", self._timeout)
        return func(self, *args, **kwargs)

    return decorator


def inject_pagination_params(func):
    """Decorator which injects pagination params into function.

    This decorator pops out such parameters as ``page``, ``per_page``,
    ``all_items``, ``filter`` and ``sort_by`` and prepares correct
    ``query_params`` unified parameter which should be used for
    as a parameter of decorated function.

    :return: Value of decorated function.
    """

    @six.wraps(func)
    def decorator(*args, **kwargs):
        params = make_query_params(
            page=kwargs.pop("page", None),
            per_page=kwargs.pop("per_page", None),
            all=kwargs.pop("all_items", None),
            filter=kwargs.pop("filter", None),
            sort_by=kwargs.pop("sort_by", None)
        )

        if "all" in params:
            params["all"] = str(int(bool(params["all"])))
        if "filter" in params:
            params["filter"] = json_dumps(params["filter"])
        if "sort_by" in params:
            params["sort_by"] = json_dumps(params["sort_by"])

        kwargs["query_params"] = params

        return func(*args, **kwargs)

    return decorator


def no_auth(func):
    """Decorator which injects mark that no authentication should
    be performed for this API call.

    :return: Value of decorated function.
    """

    @six.wraps(func)
    def decorator(*args, **kwargs):
        kwargs["auth"] = auth.no_auth
        return func(*args, **kwargs)

    return decorator


def wrap_errors(func):
    """Decorator which logs and catches all errors of decorated function.

    Also wraps all possible errors into :py:exc:`DecapodAPIError` class.

    :return: Value of decorated function.
    :raises decapodlib.exceptions.DecapodError: on any exception in
        decorated function.
    """

    @six.wraps(func)
    def decorator(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as exc:
            if isinstance(exc, exceptions.DecapodError):
                LOG.error("Error on access to API: %s", exc)
                raise

            LOG.exception("Exception in decapodlib: %s", exc)

            raise exceptions.DecapodAPIError(exc)

    return decorator


def client_metaclass(name, bases, attrs):
    """A client metaclass to create client instances.

    Basically, it just wraps all public methods with
    :py:func:`wrap_errors`/:py:func:`json_response` decorator pair so no
    need to explicitly define those decorators for every method.
    """

    new_attrs = {}
    for key, value in six.iteritems(attrs):
        if not key.startswith("_") and inspect.isfunction(value):
            value = json_response(value)
            value = wrap_errors(value)
            value = inject_timeout(value)
        new_attrs[key] = value

    return type(name, bases, new_attrs)


class HTTPAdapter(requests.adapters.HTTPAdapter):
    """HTTP adapter for client's :py:class:`requests.Session` which injects
    correct User-Agent header for request."""

    USER_AGENT = "decapodlib/{0}".format(VERSION)
    """User agent for :py:class:`decapodlib.client.Client` instance.

    As a rule, it is just ``decapodlib/{version}`` string.
    """

    def add_headers(self, request, **kwargs):
        request.headers["User-Agent"] = self.USER_AGENT
        super(HTTPAdapter, self).add_headers(request, **kwargs)


@six.add_metaclass(abc.ABCMeta)
@six.python_2_unicode_compatible
[docs]class Client(object): """A base RPC client model. :param str url: URL of Decapod API (*without* version prefix like ``/v1``). :param str login: Login of user in Decapod. :param str password: Password of user in Decapod. :param timeout: Timeout for remote requests. If ``None`` is set, default socket timeout (e.g which is set by :py:func:`socket.setdefaulttimeout`) will be used. :param bool verify: If remote URL implies SSL, then using this option client will check SSL certificate for validity. :param certificate_file: If SSL works with client certificate, this option sets the path to such certificate. If ``None`` is set, then it implies that no client certificate should be used. :type timeout: :py:class:`int` or ``None`` :type certificate_file: :py:class:`str` or ``None`` """ AUTH_CLASS = None """Base class for authenication.""" @staticmethod def _prepare_base_url(url): """Prepares base url to be used further.""" url = url.strip().rstrip("/") if not url.startswith("http"): url = "http://{0}".format(url) return url def __init__(self, url, login, password, timeout=None, verify=True, certificate_file=None): self._url = self._prepare_base_url(url) self._login = login self._password = password self._session = requests.Session() self._timeout = timeout or socket.getdefaulttimeout() or None adapter = HTTPAdapter() self._session.mount("http://", adapter) self._session.mount("https://", adapter) self._session.verify = bool(verify) if verify and certificate_file: self._session.cert = certificate_file if self.AUTH_CLASS: self._session.auth = self.AUTH_CLASS(self) def _make_url(self, endpoint): """Concatenates base url and endpoint.""" url = "{0}{1}".format(self._url, endpoint) if not url.endswith("/"): url += "/" return url @abc.abstractmethod def login(self, **kwargs): raise NotImplementedError() def __str__(self): return "DecapodAPI: url={0!r}, login={1!r}, password={2!r}".format( self._url, self._login, "*" * len(self._password) ) def __repr__(self): return "<{0}(url={1!r}, login={2!r}, password={3!r})>".format( self.__class__.__name__, self._url, self._login, "*" * len(self._password) )
@six.add_metaclass(client_metaclass)
[docs]class V1Client(Client): """Implemetation of :py:class:`decapodlib.client.Client` which works with API version 1. Please check parameters for :py:class:`decapodlib.client.Client` class. .. note:: All ``**kwargs`` keyword arguments here are the same as :py:meth:`requests.Session.request` takes. """ AUTH_CLASS = auth.V1Auth
[docs] def login(self, **kwargs): """This methods logins users into API. Basically, you do not need to execute this method by yourself, client will implicitly execute it when needed. This method does ``POST /v1/auth`` endpoint call. :return: Model of the Token. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url(self.AUTH_CLASS.AUTH_URL) payload = { "username": self._login, "password": self._password } response = self._session.post(url, json=payload, **kwargs) return response
[docs] def logout(self, **kwargs): """This method logouts users from API (after that security token will be deleted). Basically, you do not need to execute this method by yourself, client will implicitly execute it when needed. This method does ``DELETE /v1/auth`` endpoint call. :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ if not self._session.auth.token: return {} url = self._make_url(self.AUTH_CLASS.AUTH_URL) try: return self._session.delete(url, **kwargs) except Exception: return {} finally: self._session.auth.revoke_token()
@inject_pagination_params
[docs] def get_clusters(self, query_params, **kwargs): """This method fetches a list of latest cluster models from API. By default, only active clusters will be listed. This method does ``GET /v1/cluster`` endpoint call. :return: List of latest cluster models. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/cluster/") return self._session.get(url, params=query_params, **kwargs)
[docs] def get_cluster(self, cluster_id, **kwargs): """This method fetches a single cluster model (latest version) from API. This method does ``GET /v1/cluster/{cluster_id}`` endpoint call. :param str cluster_id: UUID4 (:rfc:`4122`) in string form of cluster's ID :return: Cluster model of latest available version :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/cluster/{0}/".format(cluster_id)) return self._session.get(url, **kwargs)
@inject_pagination_params
[docs] def get_cluster_versions(self, cluster_id, query_params, **kwargs): """This method fetches a list of all versions for a certain cluster model. This method does ``GET /v1/cluster/{cluster_id}/version/`` endpoint call. :param str cluster_id: UUID4 (:rfc:`4122`) in string form of cluster's ID :return: List of cluster versions for cluster with ID ``cluster_id``. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/cluster/{0}/version/".format(cluster_id)) return self._session.get(url, params=query_params, **kwargs)
[docs] def get_cluster_version(self, cluster_id, version, **kwargs): """This method fetches a certain version of particular cluster model. This method does ``GET /v1/cluster/{cluster_id}/version/{version}`` endpoint call. :param str cluster_id: UUID4 (:rfc:`4122`) in string form of cluster's ID :param int version: The number of version to fetch. :return: Cluster model of certain version. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/cluster/{0}/version/{1}/".format(cluster_id, version)) return self._session.get(url, **kwargs)
[docs] def create_cluster(self, name, **kwargs): """This method creates new cluster model. This method does ``POST /v1/cluster/`` endpoint call. :param str name: Name of the cluster. :return: New cluster model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/cluster/") payload = { "name": name } return self._session.post(url, json=payload, **kwargs)
[docs] def update_cluster(self, model_data, **kwargs): """This methods updates cluster model. Please be noticed that no real update is performed, just a new version of the same cluster is created. This method does ``PUT /v1/cluster/`` endpoint call. :param dict model_data: Updated model of the cluster. :return: Updated cluster model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/cluster/{0}/".format(model_data["id"])) return self._session.put(url, json=model_data, **kwargs)
[docs] def delete_cluster(self, cluster_id, **kwargs): """This methods deletes cluster model. Please be noticed that no real delete is performed, cluster model is marked as deleted (``time_deleted > 0``) and model will be skipped from listing, updates are forbidden. This method does ``DELETE /v1/cluster/`` endpoint call. :param str cluster_id: UUID4 (:rfc:`4122`) in string form of cluster's ID :return: Deleted cluster model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/cluster/{0}/".format(cluster_id)) return self._session.delete(url, **kwargs)
@inject_pagination_params
[docs] def get_executions(self, query_params, **kwargs): """This method fetches a list of latest execution models from API. This method does ``GET /v1/execution`` endpoint call. :return: List of latest execution models. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/execution/") return self._session.get(url, params=query_params, **kwargs)
[docs] def get_execution(self, execution_id, **kwargs): """This method fetches a single execution model (latest version) from API. This method does ``GET /v1/execution/{execution_id}`` endpoint call. :param str execution_id: UUID4 (:rfc:`4122`) in string form of execution's ID :return: Execution model of latest available version :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/execution/{0}/".format(execution_id)) return self._session.get(url, **kwargs)
@inject_pagination_params
[docs] def get_execution_versions(self, execution_id, query_params, **kwargs): """This method fetches a list of all versions for a certain execution model. This method does ``GET /v1/execution/{execution_id}/version/`` endpoint call. :param str execution_id: UUID4 (:rfc:`4122`) in string form of execution's ID :return: List of execution versions for execution with ID ``execution_id``. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/execution/{0}/version/".format(execution_id)) return self._session.get(url, params=query_params, **kwargs)
[docs] def get_execution_version(self, execution_id, version, **kwargs): """This method fetches a certain version of particular execution model. This method does ``GET /v1/execution/{execution_id}/version/{version}`` endpoint call. :param str execution_id: UUID4 (:rfc:`4122`) in string form of execution's ID :param int version: The number of version to fetch. :return: Execution model of certain version. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/execution/{0}/version/{1}/".format(execution_id, version)) return self._session.get(url, **kwargs)
[docs] def create_execution(self, playbook_configuration_id, playbook_configuration_version, **kwargs): """This method creates new execution model. This method does ``POST /v1/execution/`` endpoint call. :param str playbook_configuration_id: UUID4 (:rfc:`4122`) in string form of playbook configuration's ID. :param int playbook_configuration_version: Version of playbook configuration model. :return: New execution model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/execution/") payload = { "playbook_configuration": { "id": playbook_configuration_id, "version": playbook_configuration_version } } return self._session.post(url, json=payload, **kwargs)
[docs] def cancel_execution(self, execution_id, **kwargs): """This method cancels existing execution. This method does ``DELETE /v1/execution/`` endpoint call. :param str execution_id: UUID4 (:rfc:`4122`) in string form of execution's ID. :return: Canceled execution model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/execution/{0}/".format(execution_id)) return self._session.delete(url, **kwargs)
@inject_pagination_params
[docs] def get_execution_steps(self, execution_id, query_params, **kwargs): """This method fetches step models of the execution. This method does ``GET /v1/execution/{execution_id}/steps`` endpoint call. :param str execution_id: UUID4 (:rfc:`4122`) in string form of execution's ID. :return: List of execution steps. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/execution/{0}/steps/".format(execution_id)) return self._session.get(url, params=query_params, **kwargs)
[docs] def get_execution_log(self, execution_id, **kwargs): """This method fetches text execution log for a certain execution. Execution log is a raw Ansible execution log, that one, which is generated by :program:`ansible-playbook` program. This method does ``GET /v1/execution/{execution_id}/log`` endpoint call. :param str execution_id: UUID4 (:rfc:`4122`) in string form of execution's ID. :return: List of execution steps. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ kwargs.setdefault("headers", {}).setdefault( "Content-Type", "application/json" ) url = self._make_url("/v1/execution/{0}/log/".format(execution_id)) return self._session.get(url, **kwargs)
@inject_pagination_params
[docs] def get_playbook_configurations(self, query_params, **kwargs): """This method fetches a list of latest playbook configuration models from API. By default, only active playbook configurations will be listed. This method does ``GET /v1/playbook_configuration`` endpoint call. :return: List of latest playbook configuration models. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/playbook_configuration/") return self._session.get(url, params=query_params, **kwargs)
[docs] def get_playbook_configuration(self, playbook_configuration_id, **kwargs): """This method fetches a single playbook configuration model (latest version) from API. This method does ``GET /v1/playbook_configuration/{playbook_configuration_id}`` endpoint call. :param str playbook_configuration_id: UUID4 (:rfc:`4122`) in string form of playbook configuration's ID. :return: Playbook configuration model of latest available version. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/playbook_configuration/{0}/".format(playbook_configuration_id) ) return self._session.get(url, **kwargs)
[docs] def get_playbook_configuration_versions(self, playbook_configuration_id, query_params, **kwargs): """This method fetches a list of all versions for a certain playbook configuration model. This method does ``GET /v1/playbook_configuration/{playbook_configuration_id}/version/`` endpoint call. :param str playbook_configuration_id: UUID4 (:rfc:`4122`) in string form of playbook configuration's ID. :return: List of playbook configuration versions for playbook configuration with ID ``playbook_configuration_id``. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/playbook_configuration/{0}/version/".format( playbook_configuration_id)) return self._session.get(url, params=query_params, **kwargs)
[docs] def get_playbook_configuration_version(self, playbook_configuration_id, version, **kwargs): """This method fetches a certain version of particular playbook configuration model. This method does ``GET /v1/playbook_configuration/{playbook_configuration_id}/version/{version}`` endpoint call. :param str playbook_configuration_id: UUID4 (:rfc:`4122`) in string form of playbook configuration's ID :param int version: The number of version to fetch. :return: Playbook configuration model of certain version. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/playbook_configuration/{0}/version/{1}/".format( playbook_configuration_id, version)) return self._session.get(url, **kwargs)
[docs] def create_playbook_configuration(self, name, cluster_id, playbook_id, server_ids, hints=None, **kwargs): """This method creates new playbook configuration model. This method does ``POST /v1/playbook_configuration/`` endpoint call. Hints for playbook configuration are the list of optional parameters for creating playbook configuration. It has to be the list key/value parameters obtained from :py:meth:`decapodlib.client.V1Client.get_playbooks`. .. code-block:: json [ { "id": "dmcrypt", "value": true } ] :param str name: Name of the playbook configuration. :param str cluster_id: UUID4 (:rfc:`4122`) in string form of cluster's ID :param str playbook_id: ID of playbook to use. :param server_ids: List of server UUID4 (:rfc:`4122`) in string form of server model IDs. :type server_ids: [:py:class:`str`, ...] :param list hints: List of hints for playbook configuration. :return: New cluster model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/playbook_configuration/") payload = { "name": name, "cluster_id": cluster_id, "playbook_id": playbook_id, "server_ids": list(set(server_ids)), "hints": hints or [] } return self._session.post(url, json=payload, **kwargs)
[docs] def update_playbook_configuration(self, model_data, **kwargs): """This method updates playbook configuration model. Please be noticed that no real update is performed, just a new version of the same playbook configuration is created. This method does ``PUT /v1/playbook_configuration/`` endpoint call. :param dict model_data: Updated model of the playbook configuration. :return: Updated playbook configuration model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/playbook_configuration/{0}/".format(model_data["id"])) return self._session.put(url, json=model_data, **kwargs)
[docs] def delete_playbook_configuration(self, playbook_configuration_id, **kwargs): """This method deletes playbook configuration model. Please be noticed that no real delete is performed, playbook configuration model is marked as deleted (``time_deleted > 0``) and model will be skipped from listing, updates are forbidden. This method does ``DELETE /v1/playbook_configuration/`` endpoint call. :param str playbook_configuration_id: UUID4 (:rfc:`4122`) in string form of playbook configuration's ID :return: Deleted playbook configuration model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/playbook_configuration/{0}/".format(playbook_configuration_id) ) return self._session.delete(url, **kwargs)
@inject_pagination_params
[docs] def get_servers(self, query_params, **kwargs): """This method fetches a list of latest server models from API. By default, only active servers will be listed. This method does ``GET /v1/server`` endpoint call. :return: List of latest server models. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/server/") return self._session.get(url, params=query_params, **kwargs)
[docs] def get_server(self, server_id, **kwargs): """This method fetches a single server model (latest version) from API. This method does ``GET /v1/server/{server_id}`` endpoint call. :param str server_id: UUID4 (:rfc:`4122`) in string form of server's ID :return: Server model of latest available version :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/server/{0}/".format(server_id)) return self._session.get(url, **kwargs)
@inject_pagination_params
[docs] def get_server_versions(self, server_id, query_params, **kwargs): """This method fetches a list of all versions for a certain server model. This method does ``GET /v1/server/{server_id}/version/`` endpoint call. :param str server_id: UUID4 (:rfc:`4122`) in string form of server's ID :return: List of server versions for server with ID ``server_id``. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/server/{0}/version/".format(server_id)) return self._session.get(url, params=query_params, **kwargs)
[docs] def get_server_version(self, server_id, version, **kwargs): """This method fetches a certain version of particular server model. This method does ``GET /v1/server/{server_id}/version/{version}`` endpoint call. :param str server_id: UUID4 (:rfc:`4122`) in string form of server's ID :param int version: The number of version to fetch. :return: Server model of certain version. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/server/{0}/version/{1}/".format(server_id, version)) return self._session.get(url, **kwargs)
[docs] def create_server(self, server_id, host, username, **kwargs): """This method creates new server model. This method does ``POST /v1/server/`` endpoint call. .. warning:: You should avoid to use this method manually. Servers must be discovered using `cloud-init <https://cloudinit.readthedocs.io/en/latest/>`_ based discovery mechanism. :param str server_id: Unique ID of server. :param str host: Hostname of the server (should be accessible by Decapod). It is better to have FQDN here. :param str username: The name of the user for Ansible on this server. Decapod will use Ansible which SSH to machine with hostname given in ``host`` parameter and that username. :return: New server model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/server/") payload = { "id": server_id, "host": host, "username": username } return self._session.post(url, json=payload, **kwargs)
[docs] def put_server(self, model_data, **kwargs): """This methods updates server model. Please be noticed that no real update is performed, just a new version of the same server is created. This method does ``PUT /v1/server/`` endpoint call. :param dict model_data: Updated model of the server. :return: Updated server model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/server/{0}/".format(model_data["id"])) return self._session.put(url, json=model_data, **kwargs)
[docs] def delete_server(self, server_id, **kwargs): """This methods deletes server model. Please be noticed that no real delete is performed, server model is marked as deleted (``time_deleted > 0``) and model will be skipped from listing, updates are forbidden. This method does ``DELETE /v1/server/`` endpoint call. :param str server_id: UUID4 (:rfc:`4122`) in string form of server's ID :return: Deleted server model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/server/{0}/".format(server_id)) return self._session.delete(url, **kwargs)
@inject_pagination_params
[docs] def get_users(self, query_params, **kwargs): """This method fetches a list of latest user models from API. By default, only active users will be listed. This method does ``GET /v1/user`` endpoint call. :return: List of latest user models. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/user/") return self._session.get(url, params=query_params, **kwargs)
[docs] def get_user(self, user_id, **kwargs): """This method fetches a single user model (latest version) from API. This method does ``GET /v1/user/{user_id}`` endpoint call. :param str user_id: UUID4 (:rfc:`4122`) in string form of user's ID :return: User model of latest available version :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/user/{0}/".format(user_id)) return self._session.get(url, **kwargs)
[docs] def get_user_self(self, **kwargs): """This methods requests model of current user. This method does ``GET /v1/user/self/`` endpoint call. :return: User model of current user. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/user/self/") return self._session.get(url, **kwargs)
@inject_pagination_params
[docs] def get_user_versions(self, user_id, query_params, **kwargs): """This method fetches a list of all versions for a certain user model. This method does ``GET /v1/user/{user_id}/version/`` endpoint call. :param str user_id: UUID4 (:rfc:`4122`) in string form of user's ID :return: List of user versions for user with ID ``user_id``. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/user/{0}/version/".format(user_id)) return self._session.get(url, params=query_params, **kwargs)
[docs] def get_user_version(self, user_id, version, **kwargs): """This method fetches a certain version of particular user model. This method does ``GET /v1/user/{user_id}/version/{version}`` endpoint call. :param str user_id: UUID4 (:rfc:`4122`) in string form of user's ID :param int version: The number of version to fetch. :return: User model of certain version. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/user/{0}/version/{1}/".format(user_id, version)) return self._session.get(url, **kwargs)
[docs] def create_user(self, login, email, full_name="", role_id=None, **kwargs): """This method creates new user model. This method does ``POST /v1/user/`` endpoint call. :param str name: Name of the user. :return: New user model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/user/") payload = { "login": login, "email": email, "full_name": full_name, "role_id": role_id } return self._session.post(url, json=payload, **kwargs)
[docs] def update_user(self, model_data, **kwargs): """This methods updates user model. Please be noticed that no real update is performed, just a new version of the same user is created. This method does ``PUT /v1/user/`` endpoint call. :param dict model_data: Updated model of the user. :return: Updated user model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/user/{0}/".format(model_data["id"])) return self._session.put(url, json=model_data, **kwargs)
[docs] def delete_user(self, user_id, **kwargs): """This methods deletes user model. Please be noticed that no real delete is performed, user model is marked as deleted (``time_deleted > 0``) and model will be skipped from listing, updates are forbidden. This method does ``DELETE /v1/user/`` endpoint call. :param str user_id: UUID4 (:rfc:`4122`) in string form of user's ID :return: Deleted user model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/user/{0}/".format(user_id)) return self._session.delete(url, **kwargs)
@inject_pagination_params
[docs] def get_roles(self, query_params, **kwargs): """This method fetches a list of latest role models from API. By default, only active roles will be listed. This method does ``GET /v1/role`` endpoint call. :return: List of latest role models. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/role/") return self._session.get(url, params=query_params, **kwargs)
[docs] def get_role(self, role_id, **kwargs): """This method fetches a single role model (latest version) from API. This method does ``GET /v1/role/{role_id}`` endpoint call. :param str role_id: UUID4 (:rfc:`4122`) in string form of role's ID :return: Role model of latest available version :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/role/{0}/".format(role_id)) return self._session.get(url, **kwargs)
[docs] def get_role_self(self, **kwargs): """This methods requests model of role of current user. This method does ``GET /v1/role/self/`` endpoint call. :return: Role model of current user. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/role/self/") return self._session.get(url, **kwargs)
@inject_pagination_params
[docs] def get_role_versions(self, role_id, query_params, **kwargs): """This method fetches a list of all versions for a certain role model. This method does ``GET /v1/role/{role_id}/version/`` endpoint call. :param str role_id: UUID4 (:rfc:`4122`) in string form of role's ID :return: List of role versions for role with ID ``role_id``. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/role/{0}/version/".format(role_id)) return self._session.get(url, params=query_params, **kwargs)
[docs] def get_role_version(self, role_id, version, **kwargs): """This method fetches a certain version of particular role model. This method does ``GET /v1/role/{role_id}/version/{version}`` endpoint call. :param str role_id: UUID4 (:rfc:`4122`) in string form of role's ID :param int version: The number of version to fetch. :return: Role model of certain version. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url( "/v1/role/{0}/version/{1}/".format(role_id, version)) return self._session.get(url, **kwargs)
[docs] def create_role(self, name, permissions, **kwargs): """This method creates new role model. This method does ``POST /v1/role`` endpoint call. This method accepts parameter ``permissions``. This is a list of permissions like that: .. code-block:: json [ { "name": "playbook", "permissions": [ "add_osd", "cluster_deploy", "hello_world", "purge_cluster", "remove_osd" ] }, { "name": "api", "permissions": [ "create_cluster", "create_execution", "create_playbook_configuration", "create_role", "create_server", "create_user", "delete_cluster", "delete_execution", "delete_playbook_configuration", "delete_role", "delete_server", "delete_user", "edit_cluster", "edit_playbook_configuration", "edit_role", "edit_server", "edit_user", "view_cluster", "view_cluster_versions", "view_execution", "view_execution_steps", "view_execution_version", "view_playbook_configuration", "view_playbook_configuration_version", "view_role", "view_role_versions", "view_server", "view_server_versions", "view_user", "view_user_versions" ] } ] So, each element is a dict with ``name`` and ``permissions`` field. :param str name: Name of the role. :param list permissions: A list of permissions. Please check example above. :return: New role model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/role/") payload = { "name": name, "permissions": permissions } return self._session.post(url, json=payload, **kwargs)
[docs] def update_role(self, model_data, **kwargs): """This methods updates role model. Please be noticed that no real update is performed, just a new version of the same role is created. This method does ``PUT /v1/role/`` endpoint call. :param dict model_data: Updated model of the role. :return: Updated role model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/role/{0}/".format(model_data["id"])) return self._session.put(url, json=model_data, **kwargs)
[docs] def delete_role(self, role_id, **kwargs): """This methods deletes role model. Please be noticed that no real delete is performed, role model is marked as deleted (``time_deleted > 0``) and model will be skipped from listing, updates are forbidden. This method does ``DELETE /v1/role/`` endpoint call. :param str role_id: UUID4 (:rfc:`4122`) in string form of role's ID :return: Deleted role model. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/role/{0}/".format(role_id)) return self._session.delete(url, **kwargs)
[docs] def get_permissions(self, **kwargs): """This method lists exisiting permissions in system. Not those, which available for current user, but overall ones. This is mostly required if you compose new role. This method does ``GET /v1/permission`` endpoint call. *Example of result*: .. code-block:: json { "items": [ { "name": "api", "permissions": [ "create_cluster", "create_execution", "create_playbook_configuration", "create_role", "create_server", "create_user", "delete_cluster", "delete_execution", "delete_playbook_configuration", "delete_role", "delete_server", "delete_user", "edit_cluster", "edit_playbook_configuration", "edit_role", "edit_server", "edit_user", "view_cluster", "view_cluster_versions", "view_execution", "view_execution_steps", "view_execution_version", "view_playbook_configuration", "view_playbook_configuration_version", "view_role", "view_role_versions", "view_server", "view_server_versions", "view_user", "view_user_versions" ] }, { "name": "playbook", "permissions": [ "add_osd", "cluster_deploy", "hello_world", "purge_cluster", "remove_osd" ] } ] } .. note:: As you can see, there are 2 types of permissions in Decapod: 1. api 2. playbook *api* permissions are responsible for accessing API endpoints. If user wants to access some API endpoint, he has to have appropriate permission in his role. Some endpoints require several permissions and rule of thumb here is common sense: is user wants to *update* role, he has to have a permission to *view* it. *playbook* permissions are slightly different beasts. Each permission allows user to execute a certain playbook. :return: A list of premissions like those mentioned above :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/permission/") return self._session.get(url, **kwargs)
[docs] def get_playbooks(self, **kwargs): """This method returns a list of playbooks avaialble for execution. This method does ``GET /v1/playbook`` endpoint call. *Example of result*: .. code-block:: json { "items": [ { "description": "Adding new OSD to the cluster.", "id": "add_osd", "name": "Add OSD to Ceph cluster", "required_server_list": true, "hints": [] }, { "description": "Ceph cluster deployment playbook.", "id": "cluster_deploy", "name": "Deploy Ceph cluster", "required_server_list": true, "hints": [ { "description": "Setup OSDs with dmcrypt", "id": "dmcrypt", "type": "boolean", "values": [] } ] }, { "description": "Example plugin for playbook.", "id": "hello_world", "name": "Hello World", "required_server_list": false "hints": [] }, { "description": "Purge whole Ceph cluster.", "id": "purge_cluster", "name": "Purge cluster", "required_server_list": false, "hints": [] }, { "description": "Remove OSD host from cluster.", "id": "remove_osd", "name": "Remove OSD host from Ceph cluster", "required_server_list": true, "hints": [] } ] } .. note:: Please remember that ``playbook`` parameter in ``POST /v1/playbook_configuration`` is ``id`` field here. :return: A list of playbook data. :rtype: list :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/playbook/") return self._session.get(url, **kwargs)
@no_auth
[docs] def get_info(self, **kwargs): """This method fetches basic data from Decapod API. It makes no sense to use this method for anything, it is just a healthcheck that service actually works. *Example of result*: .. code-block:: json { "time": { "local": "2016-11-16T12:46:55.868153", "unix": 1479300415, "utc": "2016-11-16T12:46:55.868220" }, "version": "0.1.0" } .. important:: This method is basically the only one you may access being not logged in. :return: Something :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/info/") return self._session.get(url, **kwargs)
@no_auth
[docs] def request_password_reset(self, login, **kwargs): """This method requests password resetting for a user. Please be noticed that no real password resetting is occured, it just *requesting* password reset. After that, user will receive secret link on his email. If user will proceed that link, he can *actually* reset her password. This method does ``POST /v1/password_reset`` endpoint call. *Example of result*: .. code-block:: json { "message": "Password reset was requested." } :param str login: Login of user who is required to reset password. :return: A message that password reset was requested. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/password_reset/") payload = {"login": login} return self._session.post(url, json=payload, **kwargs)
@no_auth
[docs] def peek_password_reset(self, reset_token, **kwargs): """This method checks if password reset with given token is still requested. It does not consume token, it just checks if it is possible or not. *Example of result*: .. code-block:: json { "message": "Password reset was requested." } :param str reset_token: Password reset token from email. :return: A message that password reset was requested. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/password_reset/{0}/".format(reset_token)) return self._session.get(url, **kwargs)
@no_auth
[docs] def reset_password(self, reset_token, new_password, **kwargs): """This method does actual password resetting. *Example of result*: .. code-block:: json { "message": "Password has been reset." } :param str reset_token: Password reset token from email. :param str new_password: New password for user. :return: A message that password was reset. :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ url = self._make_url("/v1/password_reset/{0}/".format(reset_token)) payload = {"password": new_password} return self._session.post(url, json=payload, **kwargs)
[docs] def get_cinder_integration(self, cluster_id, root="/etc/ceph", **kwargs): """This method fetches data for integration with Cinder. This method does ``GET /v1/cinder_integration/{cluster_id}`` endpoint call. :param str cluster_id: UUID4 (:rfc:`4122`) in string form of cluster's ID :param str root: Root on file system where files should be stored. :return: Integration data :rtype: dict :raises decapodlib.exceptions.DecapodError: if not possible to connect to API. :raises decapodlib.exceptions.DecapodAPIError: if API returns error response. """ params = make_query_params(root=root or None) url = self._make_url("/v1/cinder_integration/{0}/".format(cluster_id)) return self._session.get(url, params=params, **kwargs)