├── .checkignore ├── .codeclimate.yml ├── .coveragerc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bootstrap ├── consulate.iml ├── consulate ├── __init__.py ├── adapters.py ├── api │ ├── __init__.py │ ├── acl.py │ ├── agent.py │ ├── base.py │ ├── catalog.py │ ├── coordinate.py │ ├── event.py │ ├── health.py │ ├── kv.py │ ├── lock.py │ ├── session.py │ └── status.py ├── cli.py ├── client.py ├── exceptions.py ├── models │ ├── __init__.py │ ├── acl.py │ ├── agent.py │ └── base.py └── utils.py ├── docker-compose.yml ├── docs ├── Makefile ├── acl.rst ├── agent.rst ├── catalog.rst ├── conf.py ├── consul.rst ├── coordinate.rst ├── events.rst ├── health.rst ├── history.rst ├── index.rst ├── kv.rst ├── lock.rst ├── session.rst └── status.rst ├── requires ├── installation.txt ├── optional.txt └── testing.txt ├── setup.cfg ├── setup.py ├── testing └── consul.json └── tests ├── __init__.py ├── acl_tests.py ├── agent_tests.py ├── api_tests.py ├── base.py ├── base_model_tests.py ├── catalog_tests.py ├── coordinate_tests.py ├── event_tests.py ├── kv_tests.py ├── lock_tests.py ├── session_tests.py └── utils_tests.py /.checkignore: -------------------------------------------------------------------------------- 1 | tests 2 | docs 3 | setup.py 4 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Python: true 3 | exclude_paths: 4 | - docs/* 5 | - tests/* 6 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = consulate/cli.py 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | .coverage 4 | .idea 5 | .DS_Store 6 | .tox 7 | build 8 | dist 9 | env 10 | tests/cover 11 | cover 12 | docs/_build 13 | MANIFEST 14 | venv 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | language: python 7 | 8 | env: 9 | global: 10 | - PATH=$HOME/.local/bin:$PATH 11 | - AWS_DEFAULT_REGION=us-east-1 12 | - secure: "GxBrm8hKFLazshbB3c+rQUQYHAim/rm8KtBXz6uNWRvsccAK6otc5Wcz01kj679d9C9L3I/G6/wCxfgglQp3YMGKlLT+jHQpOMdDauEdeKvaUuPh3hDQBPSAUhIpycgxz9AghdWqOQGkpQQqNW9UZRC42SW2eIyWFIlQf6W31Vc=" 13 | - secure: "YdDXZGAOp1WZenhfZnHhL9cOeUQbvZZOr8FWWJoJz3EdTbf6jE2UuK4HU6CgoYDpfFlc7geah2xARr/2rkN716Rz4ZzeHBtNXfwqqQRWRLkB8J73Nowmm+6J+skv6meJyCHtcHalpv7pAH5QPn3zMK2EAogdcDJmd5GSVBjwPoo=" 14 | 15 | stages: 16 | - test 17 | - name: upload coverage 18 | - name: deploy 19 | if: tag IS present 20 | 21 | install: 22 | - pip install awscli 23 | - pip install -r requires/testing.txt 24 | - python setup.py develop 25 | 26 | before_script: 27 | - ./bootstrap 28 | - source build/test-environment 29 | 30 | script: nosetests 31 | 32 | after_success: 33 | - aws s3 cp .coverage "s3://com-gavinroy-travis/consulate/$TRAVIS_BUILD_NUMBER/.coverage.${TRAVIS_PYTHON_VERSION}" 34 | 35 | jobs: 36 | include: 37 | - python: 2.7 38 | - python: 3.4 39 | - python: 3.5 40 | - python: 3.6 41 | - python: pypy 42 | - python: pypy3 43 | - stage: upload coverage 44 | if: repo IS gmr/consulate 45 | sudo: false 46 | services: [] 47 | python: 3.6 48 | install: 49 | - pip install awscli coverage codecov 50 | before_script: true 51 | script: 52 | - mkdir coverage 53 | - aws s3 cp --recursive s3://com-gavinroy-travis/consulate/$TRAVIS_BUILD_NUMBER/ 54 | coverage 55 | - cd coverage 56 | - coverage combine 57 | - cd .. 58 | - mv coverage/.coverage . 59 | - coverage report 60 | after_success: codecov 61 | - stage: deploy 62 | sudo: false 63 | if: repo IS gmr/consulate 64 | python: 3.6 65 | services: [] 66 | install: true 67 | before_script: true 68 | script: true 69 | after_success: true 70 | deploy: 71 | distributions: sdist bdist_wheel 72 | provider: pypi 73 | user: crad 74 | on: 75 | tags: true 76 | all_branches: true 77 | password: 78 | secure: "W3mHi2tzX34KcN82tdVPZwS0RHBGkI2Cy/df18ehsVKoOzWzQvaQKh2Jh1LIBOvdZCyih+KltmMNT8ounqUd+ql6kBFd0kotQk3C3x5R5KlpbhQ4BNKp++grs+iycPwK6qnJISypt2d3ykJNgqUgfBI0p+7XVpMkBY0GIXGNcvk=" 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Test Coverage 4 | 5 | To contribute to Consulate, please make sure that any new features or changes 6 | to existing functionality **include test coverage**. 7 | 8 | *Pull requests that add or change code without coverage have a much lower chance 9 | of being accepted.* 10 | 11 | ## Prerequisites 12 | 13 | Consulate test suite has a couple of requirements: 14 | 15 | * Dependencies from [requires/testing.txt](requires/testing.txt) are installed 16 | * Local Docker and [docker-compose](https://docs.docker.com/compose/) 17 | 18 | ## Installing Dependencies 19 | 20 | You may want to develop in a virtual environment. This is usually done inside the source 21 | repository, and `.gitignore` is configured to ignore a virtual environment in `env`. 22 | 23 | ```bash 24 | python3 -m venv env 25 | source env/bin/activate 26 | ``` 27 | 28 | To install the dependencies needed to run Consulate tests, use 29 | 30 | ```bash 31 | pip install -r requires/testing.txt 32 | ``` 33 | 34 | ## Starting the test dependency 35 | 36 | Prior to running tests, ensure that Consul is running via Docker using: 37 | 38 | ```bash 39 | ./bootstrap 40 | ``` 41 | 42 | This script uses [docker-compose](https://docs.docker.com/compose/) to launch a Consul server container that is 43 | pre-configured for the tests. In addition, it configures `build/test-environment` that is loaded 44 | by the tests with configuration information for connecting to Consul. 45 | 46 | ## Running Tests 47 | 48 | To run all test suites, run: 49 | 50 | nosetests 51 | 52 | ## Code Formatting 53 | 54 | Please format your code using [yapf](http://pypi.python.org/pypi/yapf) 55 | with ``pep8`` style prior to issuing your pull request. In addition, run 56 | ``flake8`` to look for any style errors prior to submitting your PR. 57 | 58 | Both are included when the test requirements are installed. If you are fixing 59 | formatting for existing code, please separate code-reformatting commits from 60 | functionality changes. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2018 AWeber Communications, Gavin M. Roy 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the copyright holder nor the names of its contributors may 13 | be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 25 | OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Consulate: A Consul Client Library 2 | ================================== 3 | 4 | Consulate is a Python client library and set of application for the Consul 5 | service discovery and configuration system. 6 | 7 | |Version| |Status| |Coverage| 8 | 9 | Installation 10 | ------------ 11 | 12 | Consulate is available via via `pypi `_ 13 | and can be installed with easy_install or pip: 14 | 15 | .. code:: bash 16 | 17 | pip install consulate 18 | 19 | If you require communicating with Consul via a Unix socket, there is an extra 20 | dependency that is installed via: 21 | 22 | .. code:: bash 23 | 24 | pip install consulate[unixsocket] 25 | 26 | Command Line Utilities 27 | ---------------------- 28 | Consulate comes with two command line utilities that make working with Consul 29 | easier from a management perspective. The ``consulate`` application provides 30 | a cli wrapper for common tasks performed. 31 | 32 | consulate 33 | ^^^^^^^^^ 34 | The consulate application provides a CLI interface for registering a service, 35 | backing up and restoring the contents of the KV database, and actions for getting, 36 | setting, and deleting keys from the KV database. 37 | 38 | .. code:: bash 39 | 40 | usage: consulate [-h] [--api-scheme API_SCHEME] [--api-host API_HOST] 41 | [--api-port API_PORT] [--datacenter DC] [--token TOKEN] 42 | {register,deregister,kv,run_once} ... 43 | 44 | CLI utilities for Consul 45 | 46 | optional arguments: 47 | -h, --help show this help message and exit 48 | --api-scheme API_SCHEME 49 | The scheme to use for connecting to Consul with 50 | --api-host API_HOST The consul host to connect on 51 | --api-port API_PORT The consul API port to connect to 52 | --datacenter DC The datacenter to specify for the connection 53 | --token TOKEN ACL token 54 | 55 | Commands: 56 | {register,deregister,kv,run_once,services} 57 | register Register a service for this node 58 | deregister Deregister a service for this node 59 | kv Key/Value Database Utilities 60 | run_once Lock command 61 | services List services for this node 62 | 63 | If the CONSUL_RPC_ADDR environment variable is set, it will be parsed and used 64 | for default values when connecting. 65 | 66 | Service Registration Help: 67 | 68 | .. code:: bash 69 | 70 | usage: consulate register [-h] [-a ADDRESS] [-p PORT] [-s SERVICE_ID] 71 | [-t TAGS] 72 | name {check,httpcheck,no-check,ttl} ... 73 | 74 | positional arguments: 75 | name The service name 76 | 77 | optional arguments: 78 | -h, --help show this help message and exit 79 | -a ADDRESS, --address ADDRESS 80 | Specify an address 81 | -p PORT, --port PORT Specify a port 82 | -s SERVICE_ID, --service-id SERVICE_ID 83 | Specify a service ID 84 | -t TAGS, --tags TAGS Specify a comma delimited list of tags 85 | 86 | Service Check Options: 87 | {check,httpcheck,no-check,ttl} 88 | check Define an external script-based check 89 | httpcheck Define an HTTP-based check 90 | no-check Do not enable service monitoring 91 | ttl Define a duration based TTL check 92 | 93 | KV Database Utilities Help: 94 | 95 | .. code:: bash 96 | 97 | usage: consulate kv [-h] {backup,restore,ls,mkdir,get,set,rm} ... 98 | 99 | optional arguments: 100 | -h, --help show this help message and exit 101 | 102 | Key/Value Database Utilities: 103 | {backup,restore,ls,mkdir,get,set,rm} 104 | backup Backup to stdout or a JSON file 105 | restore Restore from stdin or a JSON file 106 | ls List all of the keys 107 | mkdir Create a folder 108 | get Get a key from the database 109 | set Set a key in the database 110 | rm Remove a key from the database 111 | 112 | Locking Operations Help: 113 | 114 | .. code:: bash 115 | 116 | usage: consulate [-h] run_once [-i INTERVAL] prefix command 117 | 118 | positional arguments: 119 | prefix the name of the lock which will be held in Consul. 120 | command the command to run 121 | 122 | optional arguments: 123 | -h, --help show this help message and exit 124 | -i, --interval hold the lock for INTERVAL seconds 125 | 126 | Service listing Help: 127 | 128 | .. code:: bash 129 | 130 | usage: consulate services [-h] [-i INDENT] 131 | 132 | optional arguments: 133 | -h, --help show this help message and exit 134 | -i INDENT, --indent INDENT 135 | The indent level for output 136 | 137 | API Usage Examples 138 | ------------------ 139 | The following examples highlight the usage of Consulate and does not document 140 | the scope of the full Consulate API. 141 | 142 | *Using Consulate with the Consul kv database:* 143 | 144 | .. code:: python 145 | 146 | consul = consulate.Consul() 147 | 148 | # Set the key named release_flag to True 149 | consul.kv['release_flag'] = True 150 | 151 | # Get the value for the release_flag, if not set, raises AttributeError 152 | try: 153 | should_release_feature = consul.kv['release_flag'] 154 | except AttributeError: 155 | should_release_feature = False 156 | 157 | # Delete the release_flag key 158 | del consul.kv['release_flag'] 159 | 160 | # Find all keys that start with "fl" 161 | consul.kv.find('fl') 162 | 163 | # Find all keys that start with "feature_flag" terminated by "/" separator 164 | consul.kv.find('feature_flag', separator='/') 165 | 166 | # Check to see if a key called "foo" is set 167 | if "foo" in consul.kv: 168 | print 'Already Set' 169 | 170 | # Return all of the items in the key/value store 171 | consul.kv.items() 172 | 173 | *Working with the Consulate.agent API:* 174 | 175 | .. code:: python 176 | 177 | consul = consulate.Consul() 178 | 179 | # Get all of the service checks for the local agent 180 | checks = consul.agent.checks() 181 | 182 | # Get all of the services registered with the local agent 183 | services = consul.agent.services() 184 | 185 | # Add a service to the local agent 186 | consul.agent.service.register('redis', 187 | port=6379, 188 | tags=['master'], 189 | ttl='10s') 190 | 191 | 192 | *Fetching health information from Consul:* 193 | 194 | .. code:: python 195 | 196 | consul = consulate.Consul() 197 | 198 | # Get the health of a individual node 199 | health = consul.health.node('my-node') 200 | 201 | # Get all checks that are critical 202 | checks = consul.health.state('critical') 203 | 204 | For more examples, check out the Consulate documentation. 205 | 206 | .. |Version| image:: https://img.shields.io/pypi/v/consulate.svg? 207 | :target: https://pypi.python.org/pypi/consulate 208 | 209 | .. |Status| image:: https://img.shields.io/travis/gmr/consulate.svg? 210 | :target: https://travis-ci.org/gmr/consulate 211 | 212 | .. |Coverage| image:: https://img.shields.io/codecov/c/github/gmr/consulate.svg? 213 | :target: https://codecov.io/github/gmr/consulate?branch=master 214 | -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # NAME 4 | # bootstrap -- initialize/update docker environment 5 | # 6 | # SYNOPSIS 7 | # bootstrap 8 | # bootstrap shellinit 9 | # 10 | # DESCRIPTION 11 | # Execute this script without parameters to build the local docker 12 | # environment. Once bootstrapped, dependent services are running 13 | # via docker-compose and the environment variables are written to 14 | # *build/test-environment* for future use. 15 | # 16 | # Running this script with the _shellinit_ command line parameter 17 | # causes it to simply interrogate the running docker environment, 18 | # update *build/test-environment*, and print the environment to 19 | # the standard output stream in a shell executable manner. This 20 | # makes the following pattern for setting environment variables 21 | # in the current shell work. 22 | # 23 | # prompt% $(./bootstrap shellinit) 24 | # 25 | # vim: set ts=2 sts=2 sw=2 et: 26 | set -e 27 | 28 | get_container() { 29 | echo $(echo "${DOCKER_COMPOSE_PREFIX}" | tr -d -- '-.')_$1_1 30 | } 31 | 32 | get_ipaddr() { 33 | docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1 34 | } 35 | 36 | get_exposed_port() { 37 | docker-compose ${COMPOSE_ARGS} port $1 $2 | cut -d: -f2 38 | } 39 | 40 | report_start() { 41 | printf "Waiting for $1 ... " 42 | } 43 | 44 | report_done() { 45 | printf "${COLOR_GREEN}done${COLOR_RESET}\n" 46 | } 47 | 48 | # Ensure Docker is Running 49 | if test -e /var/run/docker.sock 50 | then 51 | DOCKER_IP=127.0.0.1 52 | else 53 | echo "Docker environment not detected." 54 | exit 1 55 | fi 56 | 57 | # Activate the virtual environment 58 | if test -e env/bin/activate 59 | then 60 | . ./env/bin/activate 61 | fi 62 | 63 | mkdir -p build 64 | 65 | # Common constants 66 | COLOR_RESET='\033[0m' 67 | COLOR_GREEN='\033[0;32m' 68 | PREFIX=${PWD##*/} 69 | DOCKER_COMPOSE_PREFIX=${PREFIX:-${DOCKER_COMPOSE_PREFIX}} 70 | COMPOSE_ARGS="-p ${DOCKER_COMPOSE_PREFIX}" 71 | 72 | # Stop any running instances and clean up after them, then pull images 73 | docker-compose ${COMPOSE_ARGS} down --volumes --remove-orphans 74 | docker-compose ${COMPOSE_ARGS} pull 75 | docker-compose ${COMPOSE_ARGS} up -d 76 | 77 | cat > build/test-environment< 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /consulate/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consulate: A client library for Consul 3 | 4 | """ 5 | from consulate.client import Consul 6 | 7 | from consulate.exceptions import (ConsulateException, 8 | ClientError, 9 | ServerError, 10 | ACLDisabled, 11 | Forbidden, 12 | NotFound, 13 | LockFailure, 14 | RequestError) 15 | 16 | import logging 17 | from logging import NullHandler 18 | 19 | __version__ = '1.0.0' 20 | 21 | # Prevent undesired log output to the root logger 22 | logging.getLogger('consulate').addHandler(NullHandler()) 23 | 24 | 25 | __all__ = [ 26 | __version__, 27 | Consul, 28 | ConsulateException, 29 | ClientError, 30 | ServerError, 31 | ACLDisabled, 32 | Forbidden, 33 | NotFound, 34 | LockFailure, 35 | RequestError 36 | ] 37 | -------------------------------------------------------------------------------- /consulate/adapters.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | HTTP Client Library Adapters 4 | 5 | """ 6 | import json 7 | import logging 8 | import socket 9 | 10 | import requests 11 | import requests.exceptions 12 | try: 13 | import requests_unixsocket 14 | except ImportError: # pragma: no cover 15 | requests_unixsocket = None 16 | 17 | from consulate import api, exceptions, utils 18 | 19 | LOGGER = logging.getLogger(__name__) 20 | 21 | CONTENT_FORM = 'application/x-www-form-urlencoded; charset=utf-8' 22 | CONTENT_JSON = 'application/json; charset=utf-8' 23 | 24 | 25 | def prepare_data(fun): 26 | """Decorator for transforming the data being submitted to Consul 27 | 28 | :param function fun: The decorated function 29 | 30 | """ 31 | 32 | def inner(*args, **kwargs): 33 | """Inner wrapper function for the decorator 34 | 35 | :param list args: positional arguments 36 | :param dict kwargs: keyword arguments 37 | 38 | """ 39 | if kwargs.get('data'): 40 | if not utils.is_string(kwargs.get('data')): 41 | kwargs['data'] = json.dumps(kwargs['data']) 42 | elif len(args) == 3 and \ 43 | not (utils.is_string(args[2]) or args[2] is None): 44 | args = args[0], args[1], json.dumps(args[2]) 45 | return fun(*args, **kwargs) 46 | 47 | return inner 48 | 49 | 50 | class Request(object): 51 | """The Request adapter class""" 52 | 53 | def __init__(self, timeout=None, verify=True, cert=None): 54 | """ 55 | Create a new request adapter instance. 56 | 57 | :param int timeout: [optional] timeout to use while sending requests 58 | to consul. 59 | """ 60 | self.session = requests.Session() 61 | self.session.verify = verify 62 | self.session.cert = cert 63 | self.timeout = timeout 64 | 65 | def delete(self, uri): 66 | """Perform a HTTP delete 67 | 68 | :param src uri: The URL to send the DELETE to 69 | :rtype: consulate.api.Response 70 | 71 | """ 72 | LOGGER.debug("DELETE %s", uri) 73 | return api.Response(self.session.delete(uri, timeout=self.timeout)) 74 | 75 | def get(self, uri, timeout=None): 76 | """Perform a HTTP get 77 | 78 | :param src uri: The URL to send the DELETE to 79 | :param timeout: How long to wait on the response 80 | :type timeout: int or float or None 81 | :rtype: consulate.api.Response 82 | 83 | """ 84 | LOGGER.debug("GET %s", uri) 85 | try: 86 | return api.Response(self.session.get( 87 | uri, timeout=timeout or self.timeout)) 88 | except (requests.exceptions.RequestException, 89 | OSError, socket.error) as err: 90 | raise exceptions.RequestError(str(err)) 91 | 92 | def get_stream(self, uri): 93 | """Perform a HTTP get that returns the response as a stream. 94 | 95 | :param src uri: The URL to send the DELETE to 96 | :rtype: iterator 97 | 98 | """ 99 | LOGGER.debug("GET Stream from %s", uri) 100 | try: 101 | response = self.session.get(uri, stream=True) 102 | except (requests.exceptions.RequestException, 103 | OSError, socket.error) as err: 104 | raise exceptions.RequestError(str(err)) 105 | if utils.response_ok(response): 106 | for line in response.iter_lines(): # pragma: no cover 107 | yield line.decode('utf-8') 108 | 109 | @prepare_data 110 | def put(self, uri, data=None, timeout=None): 111 | """Perform a HTTP put 112 | 113 | :param src uri: The URL to send the DELETE to 114 | :param str data: The PUT data 115 | :param timeout: How long to wait on the response 116 | :type timeout: int or float or None 117 | :rtype: consulate.api.Response 118 | 119 | """ 120 | LOGGER.debug("PUT %s with %r", uri, data) 121 | headers = { 122 | 'Content-Type': CONTENT_FORM 123 | if utils.is_string(data) else CONTENT_JSON 124 | } 125 | try: 126 | return api.Response( 127 | self.session.put( 128 | uri, data=data, headers=headers, 129 | timeout=timeout or self.timeout)) 130 | except (requests.exceptions.RequestException, 131 | OSError, socket.error) as err: 132 | raise exceptions.RequestError(str(err)) 133 | 134 | 135 | class UnixSocketRequest(Request): # pragma: no cover 136 | """Use to communicate with Consul over a Unix socket""" 137 | 138 | def __init__(self, timeout=None): 139 | super(UnixSocketRequest, self).__init__(timeout) 140 | self.session = requests_unixsocket.Session() 141 | -------------------------------------------------------------------------------- /consulate/api/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul API Endpoints 3 | 4 | """ 5 | from consulate.api.acl import ACL 6 | from consulate.api.agent import Agent 7 | from consulate.api.catalog import Catalog 8 | from consulate.api.event import Event 9 | from consulate.api.health import Health 10 | from consulate.api.coordinate import Coordinate 11 | from consulate.api.kv import KV 12 | from consulate.api.lock import Lock 13 | from consulate.api.session import Session 14 | from consulate.api.status import Status 15 | from consulate.api.base import Response 16 | 17 | __all__ = [ 18 | ACL, 19 | Agent, 20 | Catalog, 21 | Event, 22 | Health, 23 | KV, 24 | Lock, 25 | Session, 26 | Status, 27 | Response 28 | ] 29 | -------------------------------------------------------------------------------- /consulate/api/acl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul ACL Endpoint Access 3 | 4 | """ 5 | import logging 6 | 7 | from consulate.models import acl as model 8 | from consulate.api import base 9 | from consulate import exceptions 10 | # from typing import List, Dict, Union 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | # ServiceIdentity = Dict[str, Union[str, List[str]]] 15 | # ServiceIdentities = List[ServiceIdentity] 16 | # PolicyLink = Dict[str, str] 17 | # PolicyLinks = List[PolicyLink] 18 | # RoleLink = Dict[str, str] 19 | # RoleLinks = List[RoleLink] 20 | 21 | 22 | class ACL(base.Endpoint): 23 | """The ACL endpoints are used to create, update, destroy, and query ACL 24 | tokens. 25 | 26 | """ 27 | def list_policies(self): 28 | """List all ACL policies available in cluster. 29 | 30 | :param rtype: list 31 | 32 | """ 33 | return self._get(["policies"]) 34 | 35 | def read_policy(self, id): 36 | """Read an existing policy with the given ID. 37 | 38 | :param str id: The ID of the policy. 39 | :param rtype: dict 40 | 41 | """ 42 | return self._get(["policy", id]) 43 | 44 | def create_policy(self, 45 | name, 46 | datacenters=None, 47 | description=None, 48 | rules=None): 49 | """Create policy with name given and rules. 50 | 51 | :param str name: name of the policy 52 | :param list() datacenters: A list of datacenters to filter on policy. 53 | :param str description: Human readable description of the policy. 54 | :param str rules: A json serializable string for ACL rules. 55 | :param rtype: dict 56 | 57 | """ 58 | return self._put_response_body(["policy"], {}, 59 | dict( 60 | model.ACLPolicy( 61 | name=name, 62 | datacenters=datacenters, 63 | description=description, 64 | rules=rules))) 65 | 66 | def update_policy(self, 67 | id, 68 | name, 69 | datacenters=None, 70 | description=None, 71 | rules=None): 72 | """Update policy with id given. 73 | 74 | :param str id: A UUID for the policy to update. 75 | :param str name: name of the policy 76 | :param list() datacenters: A list of datacenters to filter on policy. 77 | :param str description: Human readable description of the policy. 78 | :param str rules: A json serializable string for ACL rules. 79 | :param rtype: dict 80 | 81 | """ 82 | return self._put_response_body(["policy", id], {}, 83 | dict( 84 | model.ACLPolicy( 85 | name=name, 86 | datacenters=datacenters, 87 | description=description, 88 | rules=rules))) 89 | 90 | def delete_policy(self, id): 91 | """Delete an existing policy with the given ID. 92 | 93 | :param str id: The ID of the policy. 94 | :param rtype: bool 95 | 96 | """ 97 | return self._delete(["policy", id]) 98 | 99 | def list_roles(self): 100 | """List all ACL roles available in cluster 101 | :param rtype: list 102 | 103 | """ 104 | return self._get(["roles"]) 105 | 106 | def read_role(self, id=None, name=None): 107 | """Read an existing role with the given ID or Name. 108 | 109 | :param str id: The ID of the role. 110 | :param str name: The name of the role. 111 | :param rtype: dict 112 | 113 | """ 114 | if id is not None: 115 | return self._get(["role", id]) 116 | elif name is not None: 117 | return self._get(["role", "name", name]) 118 | else: 119 | raise exceptions.NotFound("Either id or name must be specified") 120 | 121 | def create_role(self, 122 | name, 123 | description=None, 124 | policies=None, 125 | service_identities=None): 126 | """Create an ACL role from a list of policies or service identities. 127 | 128 | :param str name: The name of the ACL role. Must be unique. 129 | :param str description: The description of the ACL role. 130 | :param PolicyLinks policies: An array of PolicyLink. 131 | :param ServiceIdentities service_identities: A ServiceIdentity array. 132 | :param rtype: dict 133 | 134 | """ 135 | return self._put_response_body( 136 | ["role"], {}, 137 | dict( 138 | model.ACLRole(name=name, 139 | description=description, 140 | policies=policies, 141 | service_identities=service_identities))) 142 | 143 | def update_role(self, 144 | id, 145 | name, 146 | description=None, 147 | policies=None, 148 | service_identities=None): 149 | """Update role with id given. 150 | 151 | :param str id: A UUID for the policy to update. 152 | :param str name: name of the policy 153 | :param list() datacenters: A list of datacenters to filter on policy. 154 | :param str description: Human readable description of the policy. 155 | :param str rules: A json serializable string for ACL rules. 156 | :param rtype: dict 157 | 158 | """ 159 | return self._put_response_body( 160 | ["role", id], {}, 161 | dict( 162 | model.ACLRole(name=name, 163 | description=description, 164 | policies=policies, 165 | service_identities=service_identities))) 166 | 167 | def delete_role(self, id): 168 | """Delete an existing role with the given ID. 169 | 170 | :param str id: The ID of the role. 171 | :param rtype: bool 172 | 173 | """ 174 | return self._delete(["policy", id]) 175 | 176 | def list_tokens(self): 177 | """List all ACL tokens available in cluster. 178 | 179 | :param rtype: list 180 | 181 | """ 182 | return self._get(["tokens"]) 183 | 184 | def read_token(self, accessor_id): 185 | """Read an existing token with the given ID. 186 | 187 | :param str id: The ID of the role. 188 | :param rtype: dict 189 | 190 | """ 191 | return self._get(["token", accessor_id]) 192 | 193 | def read_self_token(self): 194 | """Retrieve the currently used token. 195 | 196 | :param rtype: dict 197 | 198 | """ 199 | return self._get(["token", "self"]) 200 | 201 | def create_token(self, 202 | accessor_id=None, 203 | description=None, 204 | expiration_time=None, 205 | expiration_ttl=None, 206 | local=False, 207 | policies=None, 208 | roles=None, 209 | secret_id=None, 210 | service_identities=None): 211 | """Create a token from the roles, policies, and service identities 212 | provided. 213 | 214 | :param str accessor_id: A UUID for accessing the token. 215 | :param str description: A human-readable description of the token. 216 | :param str expiration_time: The amount of time till the token expires. 217 | :param str expiration_ttl: Sets expiration_time to creation time + 218 | expiration_ttl value. 219 | :param bool local: Whether the token is only locally available in the 220 | current datacenter or to all datacenters defined. 221 | :param PolicyLinks policies: A PolicyLink array. 222 | :param RoleLinks roles: A RoleLink array. 223 | :param str secret_id: A UUID for making requests to consul. 224 | :param ServiceIdentities service_identities: A ServiceIdentity array. 225 | :param rtype: dict 226 | 227 | """ 228 | return self._put_response_body( 229 | ["token"], {}, 230 | dict( 231 | model.ACLToken(accessor_id=accessor_id, 232 | description=description, 233 | expiration_time=expiration_time, 234 | expiration_ttl=expiration_ttl, 235 | local=local, 236 | policies=policies, 237 | roles=roles, 238 | secret_id=secret_id, 239 | service_identities=service_identities))) 240 | 241 | def update_token(self, 242 | accessor_id, 243 | description=None, 244 | expiration_time=None, 245 | expiration_ttl=None, 246 | local=False, 247 | policies=None, 248 | roles=None, 249 | secret_id=None, 250 | service_identities=None): 251 | """Create a token from the roles, policies, and service identities 252 | provided. 253 | 254 | :param str accessor_id: A UUID for accessing the token. 255 | :param str description: A human-readable description of the token. 256 | :param str expiration_time: The amount of time till the token expires. 257 | :param str expiration_ttl: Sets expiration_time to creation time + 258 | expiration_ttl value. 259 | :param bool local: Whether the token is only locally available in the 260 | current datacenter or to all datacenters defined. 261 | :param PolicyLinks policies: A PolicyLink array. 262 | :param RoleLinks roles: A RoleLink array. 263 | :param str secret_id: A UUID for making requests to consul. 264 | :param ServiceIdentities service_identities: A ServiceIdentity array. 265 | :param rtype: dict 266 | 267 | """ 268 | return self._put_response_body( 269 | ["token", accessor_id], {}, 270 | dict( 271 | model.ACLToken(accessor_id=accessor_id, 272 | description=description, 273 | expiration_time=expiration_time, 274 | expiration_ttl=expiration_ttl, 275 | local=local, 276 | policies=policies, 277 | roles=roles, 278 | secret_id=secret_id, 279 | service_identities=service_identities))) 280 | 281 | def clone_token(self, accessor_id, description=None): 282 | """Clone a token by the accessor_id. 283 | 284 | :param str accessor_id: A UUID for accessing the token. 285 | :param str description: A human-readable description of the token. 286 | :param rtype: dict 287 | 288 | """ 289 | return self._put_response_body( 290 | ["token", accessor_id, "clone"], {}, 291 | dict(model.ACLToken(description=description))) 292 | 293 | def delete_token(self, accessor_id): 294 | """Delete an existing token with the given AcccessorID. 295 | 296 | :param str id: The AccessorID of the token. 297 | :param rtype: bool 298 | 299 | """ 300 | return self._delete(["token", accessor_id]) 301 | 302 | # NOTE: Everything below here is deprecated post consul-1.4.0. 303 | 304 | def bootstrap(self): 305 | """This endpoint does a special one-time bootstrap of the ACL system, 306 | making the first management token if the acl_master_token is not 307 | specified in the Consul server configuration, and if the cluster has 308 | not been bootstrapped previously. 309 | 310 | This is available in Consul 0.9.1 and later, and requires all Consul 311 | servers to be upgraded in order to operate. 312 | 313 | You can detect if something has interfered with the ACL bootstrapping 314 | by the response of this method. If you get a string response with the 315 | ``ID``, the bootstrap was a success. If the method raises a 316 | :exc:`~consulate.exceptions.Forbidden` exception, the cluster has 317 | already been bootstrapped, at which point you should consider the 318 | cluster in a potentially compromised state. 319 | 320 | .. versionadded: 1.0.0 321 | 322 | :rtype: str 323 | :raises: :exc:`~consulate.exceptions.Forbidden` 324 | 325 | """ 326 | return self._put_response_body(['bootstrap'])['ID'] 327 | 328 | def create(self, name, acl_type='client', rules=None): 329 | """The create endpoint is used to make a new token. A token has a name, 330 | a type, and a set of ACL rules. 331 | 332 | The ``name`` property is opaque to Consul. To aid human operators, it 333 | should be a meaningful indicator of the ACL's purpose. 334 | 335 | ``acl_type`` is either client or management. A management token is 336 | comparable to a root user and has the ability to perform any action 337 | including creating, modifying, and deleting ACLs. 338 | 339 | By contrast, a client token can only perform actions as permitted by 340 | the rules associated. Client tokens can never manage ACLs. Given this 341 | limitation, only a management token can be used to make requests to 342 | the create endpoint. 343 | 344 | ``rules`` is a HCL string defining the rule policy. See 345 | `Internals on `_ ACL 346 | for more information on defining rules. 347 | 348 | The call to create will return the ID of the new ACL. 349 | 350 | :param str name: The name of the ACL to create 351 | :param str acl_type: One of "client" or "management" 352 | :param str rules: The rules HCL string 353 | :rtype: str 354 | :raises: consulate.exceptions.Forbidden 355 | 356 | """ 357 | return self._put_response_body(['create'], {}, 358 | dict( 359 | model.ACL(name=name, 360 | type=acl_type, 361 | rules=rules)))['ID'] 362 | 363 | def clone(self, acl_id): 364 | """Clone an existing ACL returning the new ACL ID 365 | 366 | :param str acl_id: The ACL id 367 | :rtype: bool 368 | :raises: consulate.exceptions.Forbidden 369 | 370 | """ 371 | return self._put_response_body(['clone', acl_id])['ID'] 372 | 373 | def destroy(self, acl_id): 374 | """Delete the specified ACL 375 | 376 | :param str acl_id: The ACL id 377 | :rtype: bool 378 | :raises: consulate.exceptions.Forbidden 379 | 380 | """ 381 | response = self._adapter.put(self._build_uri(['destroy', acl_id])) 382 | if response.status_code == 403: 383 | raise exceptions.Forbidden(response.body) 384 | return response.status_code == 200 385 | 386 | def info(self, acl_id): 387 | """Return a dict of information about the ACL 388 | 389 | :param str acl_id: The ACL id 390 | :rtype: dict 391 | :raises: consulate.exceptions.Forbidden 392 | :raises: consulate.exceptions.NotFound 393 | 394 | """ 395 | response = self._get(['info', acl_id], raise_on_404=True) 396 | if not response: 397 | raise exceptions.NotFound('ACL not found') 398 | return response 399 | 400 | def list(self): 401 | """Return a list of all ACLs 402 | 403 | :rtype: list([dict]) 404 | :raises: consulate.exceptions.Forbidden 405 | 406 | """ 407 | return self._get(['list']) 408 | 409 | def replication(self): 410 | """Return the status of the ACL replication process in the datacenter. 411 | 412 | This is intended to be used by operators, or by automation checking the 413 | health of ACL replication. 414 | 415 | .. versionadded: 1.0.0 416 | 417 | :rtype: dict 418 | :raises: consulate.exceptions.Forbidden 419 | 420 | """ 421 | return self._get(['replication']) 422 | 423 | def update(self, acl_id, name, acl_type='client', rules=None): 424 | """Update an existing ACL, updating its values or add a new ACL if 425 | the ACL ID specified is not found. 426 | 427 | The call will return the ID of the ACL. 428 | 429 | :param str acl_id: The ACL id 430 | :param str name: The name of the ACL 431 | :param str acl_type: The ACL type 432 | :param str rules: The ACL rules document 433 | :rtype: str 434 | :raises: consulate.exceptions.Forbidden 435 | 436 | """ 437 | return self._put_response_body(['update'], {}, 438 | dict( 439 | model.ACL(id=acl_id, 440 | name=name, 441 | type=acl_type, 442 | rules=rules)))['ID'] 443 | -------------------------------------------------------------------------------- /consulate/api/agent.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul Agent Endpoint Access 3 | 4 | """ 5 | from consulate.api import base 6 | from consulate.models import agent as models 7 | 8 | _TOKENS = [ 9 | 'acl_token', 10 | 'acl_agent_token', 11 | 'acl_agent_master_token', 12 | 'acl_replication_token' 13 | ] 14 | 15 | 16 | class Agent(base.Endpoint): 17 | """The Consul agent is the core process of Consul. The agent maintains 18 | membership information, registers services, runs checks, responds to 19 | queries and more. The agent must run on every node that is part of a 20 | Consul cluster. 21 | 22 | """ 23 | 24 | def __init__(self, uri, adapter, datacenter=None, token=None): 25 | """Create a new instance of the Agent class 26 | 27 | :param str uri: Base URI 28 | :param consul.adapters.Request adapter: Request adapter 29 | :param str datacenter: datacenter 30 | :param str token: Access Token 31 | 32 | """ 33 | super(Agent, self).__init__(uri, adapter, datacenter, token) 34 | self.check = Agent.Check(self._base_uri, adapter, datacenter, token) 35 | self.service = Agent.Service( 36 | self._base_uri, adapter, datacenter, token) 37 | 38 | class Check(base.Endpoint): 39 | """One of the primary roles of the agent is the management of system 40 | and application level health checks. A health check is considered to be 41 | application level if it associated with a service. A check is defined 42 | in a configuration file, or added at runtime over the HTTP interface. 43 | 44 | There are two different kinds of checks: 45 | 46 | - Script + Interval: These checks depend on invoking an external 47 | application which does the health check and 48 | exits with an appropriate exit code, 49 | potentially generating some output. A script 50 | is paired with an invocation interval 51 | (e.g. every 30 seconds). This is similar to 52 | the Nagios plugin system. 53 | 54 | - TTL: These checks retain their last known state for a given TTL. 55 | The state of the check must be updated periodically 56 | over the HTTP interface. If an external system fails to 57 | update the status within a given TTL, the check is set to 58 | the failed state. This mechanism is used to allow an 59 | application to directly report it's health. For example, 60 | a web app can periodically curl the endpoint, and if the 61 | app fails, then the TTL will expire and the health check 62 | enters a critical state. This is conceptually similar to a 63 | dead man's switch. 64 | 65 | """ 66 | 67 | def register(self, 68 | name, 69 | check_id=None, 70 | interval=None, 71 | notes=None, 72 | deregister_critical_service_after=None, 73 | args=None, 74 | docker_container_id=None, 75 | grpc=None, 76 | grpc_use_tls=None, 77 | http=None, 78 | http_method=None, 79 | header=None, 80 | timeout=None, 81 | tls_skip_verify=None, 82 | tcp=None, 83 | ttl=None, 84 | service_id=None, 85 | status=None): 86 | """Add a new check to the local agent. Checks are either a script 87 | or TTL type. The agent is responsible for managing the status of 88 | the check and keeping the Catalog in sync. 89 | 90 | :param str name: 91 | :param str check_id: 92 | :param str interval: 93 | :param str notes: 94 | :param str deregister_critical_service_after: 95 | :param str args: 96 | :param str docker_container_id: 97 | :param str grpc: 98 | :param str grpc_use_tls: 99 | :param str http: 100 | :param str http_method: 101 | :param str header: 102 | :param str timeout: 103 | :param str tls_skip_verify: 104 | :param str tcp: 105 | :param str ttl: 106 | :param str service_id: 107 | :param str status: 108 | 109 | :rtype: bool 110 | :raises: ValueError 111 | 112 | """ 113 | return self._put_no_response_body( 114 | ['register'], None, dict( 115 | models.Check( 116 | name=name, 117 | id=check_id, 118 | interval=interval, 119 | notes=notes, 120 | deregister_critical_service_after= 121 | deregister_critical_service_after, 122 | args=args, 123 | docker_container_id=docker_container_id, 124 | grpc=grpc, 125 | grpc_use_tls=grpc_use_tls, 126 | http=http, 127 | method=http_method, 128 | header=header, 129 | timeout=timeout, 130 | tls_skip_verify=tls_skip_verify, 131 | tcp=tcp, 132 | ttl=ttl, 133 | service_id=service_id, 134 | status=status))) 135 | 136 | def deregister(self, check_id): 137 | """Remove a check from the local agent. The agent will take care 138 | of deregistering the check with the Catalog. 139 | 140 | :param str check_id: The check id 141 | :rtype: bool 142 | 143 | """ 144 | return self._put_no_response_body(['deregister', check_id]) 145 | 146 | def ttl_pass(self, check_id, note=None): 147 | """This endpoint is used with a check that is of the TTL type. 148 | When this endpoint is accessed, the status of the check is set to 149 | "passing", and the TTL clock is reset. 150 | 151 | :param str check_id: The check id 152 | :param str note: Note to include with the check pass 153 | :rtype: bool 154 | 155 | """ 156 | return self._put_no_response_body( 157 | ['pass', check_id], {'note': note} if note else None) 158 | 159 | def ttl_warn(self, check_id, note=None): 160 | """This endpoint is used with a check that is of the TTL type. 161 | When this endpoint is accessed, the status of the check is set 162 | to "warning", and the TTL clock is reset. 163 | 164 | :param str check_id: The check id 165 | :param str note: Note to include with the check warning 166 | :rtype: bool 167 | 168 | """ 169 | return self._put_no_response_body( 170 | ['warn', check_id], {'note': note} if note else None) 171 | 172 | def ttl_fail(self, check_id, note=None): 173 | """This endpoint is used with a check that is of the TTL type. 174 | When this endpoint is accessed, the status of the check is set 175 | to "critical", and the TTL clock is reset. 176 | 177 | :param str check_id: The check id 178 | :param str note: Note to include with the check failure 179 | :rtype: bool 180 | 181 | """ 182 | return self._put_no_response_body( 183 | ['fail', check_id], {'note': note} if note else None) 184 | 185 | class Service(base.Endpoint): 186 | """One of the main goals of service discovery is to provide a catalog 187 | of available services. To that end, the agent provides a simple 188 | service definition format to declare the availability of a service, a 189 | nd to potentially associate it with a health check. A health check is 190 | considered to be application level if it associated with a service. A 191 | service is defined in a configuration file, or added at runtime over 192 | the HTTP interface. 193 | 194 | """ 195 | def register(self, 196 | name, 197 | service_id=None, 198 | address=None, 199 | port=None, 200 | tags=None, 201 | meta=None, 202 | check=None, 203 | checks=None, 204 | enable_tag_override=None): 205 | """Add a new service to the local agent. 206 | 207 | :param str name: The name of the service 208 | :param str service_id: The id for the service (optional) 209 | :param str address: The service IP address 210 | :param int port: The service port 211 | :param list tags: A list of tags for the service 212 | :param list meta: A list of KV pairs for the service 213 | :param check: An optional check definition for the service 214 | :type check: :class:`consulate.models.agent.Check` 215 | :param checks: A list of check definitions for the service 216 | :type checks: list([:class:`consulate.models.agent.Check`]) 217 | :param bool enable_tag_override: Toggle the tag override feature 218 | :rtype: bool 219 | :raises: ValueError 220 | 221 | """ 222 | return self._put_no_response_body( 223 | ['register'], None, 224 | dict(models.Service( 225 | name=name, id=service_id, address=address, port=port, 226 | tags=tags, meta=meta, check=check, checks=checks, 227 | enable_tag_override=enable_tag_override))) 228 | 229 | def deregister(self, service_id): 230 | """Deregister the service from the local agent. The agent will 231 | take care of deregistering the service with the Catalog. If there 232 | is an associated check, that is also deregistered. 233 | 234 | :param str service_id: The service id to deregister 235 | :rtype: bool 236 | 237 | """ 238 | return self._put_no_response_body(['deregister', service_id]) 239 | 240 | def maintenance(self, service_id, enable=True, reason=None): 241 | """Place given service into "maintenance mode". 242 | 243 | :param str service_id: The id for the service 244 | :param bool enable: Enable maintenance mode 245 | :param str reason: Reason for putting node in maintenance 246 | :rtype: bool 247 | 248 | """ 249 | query_params = {'enable': enable} 250 | if reason: 251 | query_params['reason'] = reason 252 | return self._put_no_response_body(['maintenance', service_id], 253 | query_params) 254 | 255 | def checks(self): 256 | """Return the all the checks that are registered with the local agent. 257 | These checks were either provided through configuration files, or 258 | added dynamically using the HTTP API. It is important to note that 259 | the checks known by the agent may be different than those reported 260 | by the Catalog. This is usually due to changes being made while there 261 | is no leader elected. The agent performs active anti-entropy, so in 262 | most situations everything will be in sync within a few seconds. 263 | 264 | :rtype: dict 265 | 266 | """ 267 | return self._get(['checks']) 268 | 269 | def force_leave(self, node): 270 | """Instructs the agent to force a node into the left state. If a node 271 | fails unexpectedly, then it will be in a "failed" state. Once in this 272 | state, Consul will attempt to reconnect, and additionally the services 273 | and checks belonging to that node will not be cleaned up. Forcing a 274 | node into the left state allows its old entries to be removed. 275 | 276 | """ 277 | return self._put_no_response_body(['force-leave', node]) 278 | 279 | def join(self, address, wan=False): 280 | """This endpoint is hit with a GET and is used to instruct the agent 281 | to attempt to connect to a given address. For agents running in 282 | server mode, setting wan=True causes the agent to attempt to join 283 | using the WAN pool. 284 | 285 | :param str address: The address to join 286 | :param bool wan: Join a WAN pool as a server 287 | :rtype: bool 288 | 289 | """ 290 | query_params = {'wan': 1} if wan else None 291 | return self._put_no_response_body(['join', address], query_params) 292 | 293 | def maintenance(self, enable=True, reason=None): 294 | """Places the agent into or removes the agent from "maintenance mode". 295 | 296 | .. versionadded:: 1.0.0 297 | 298 | :param bool enable: Enable or disable maintenance. Default: `True` 299 | :param str reason: The reason for the maintenance 300 | :rtype: bool 301 | 302 | """ 303 | query_params = {'enable': enable} 304 | if reason: 305 | query_params['reason'] = reason 306 | return self._put_no_response_body(['maintenance'], query_params) 307 | 308 | def members(self): 309 | """Returns the members the agent sees in the cluster gossip pool. 310 | Due to the nature of gossip, this is eventually consistent and the 311 | results may differ by agent. The strongly consistent view of nodes 312 | is instead provided by ``Consulate.catalog.nodes``. 313 | 314 | :rtype: list 315 | 316 | """ 317 | return self._get_list(['members']) 318 | 319 | def metrics(self): 320 | """Returns agent's metrics for the most recent finished interval 321 | 322 | .. versionadded:: 1.0.0 323 | 324 | :rtype: dict 325 | 326 | """ 327 | return self._get(['metrics']) 328 | 329 | def monitor(self): 330 | """Iterator over logs from the local agent. 331 | 332 | .. versionadded:: 1.0.0 333 | 334 | :rtype: iterator 335 | 336 | """ 337 | for line in self._get_stream(['monitor']): 338 | yield line 339 | 340 | def reload(self): 341 | """This endpoint instructs the agent to reload its configuration. 342 | Any errors encountered during this process are returned. 343 | 344 | .. versionadded:: 1.0.0 345 | 346 | :rtype: list 347 | 348 | """ 349 | return self._put_response_body(['reload']) or None 350 | 351 | def services(self): 352 | """return the all the services that are registered with the local 353 | agent. These services were either provided through configuration 354 | files, or added dynamically using the HTTP API. It is important to 355 | note that the services known by the agent may be different than those 356 | ]reported by the Catalog. This is usually due to changes being made 357 | while there is no leader elected. The agent performs active 358 | anti-entropy, so in most situations everything will be in sync 359 | within a few seconds. 360 | 361 | :rtype: dict 362 | 363 | """ 364 | return self._get(['services']) 365 | 366 | def self(self): 367 | """ This endpoint is used to return the configuration and member 368 | information of the local agent under the Config key. 369 | 370 | :rtype: dict 371 | 372 | """ 373 | return self._get(['self']) 374 | 375 | def token(self, name, value): 376 | """Update the ACL tokens currently in use by the agent. It can be used 377 | to introduce ACL tokens to the agent for the first time, or to update 378 | tokens that were initially loaded from the agent's configuration. 379 | Tokens are not persisted, so will need to be updated again if the agent 380 | is restarted. 381 | 382 | Valid names: 383 | 384 | - ``acl_token`` 385 | - ``acl_agent_token`` 386 | - ``acl_agent_master_token`` 387 | - ``acl_replication_token`` 388 | 389 | .. versionadded:: 1.0.0 390 | 391 | :param str name: One of the valid token names. 392 | :param str value: The new token value 393 | :rtype: bool 394 | :raises: ValueError 395 | 396 | """ 397 | if name not in _TOKENS: 398 | raise ValueError('Invalid token name: {}'.format(name)) 399 | return self._put_no_response_body( 400 | ['token', name], {}, {'Token': value}) 401 | -------------------------------------------------------------------------------- /consulate/api/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base Endpoint class used by all endpoint classes 3 | 4 | """ 5 | import base64 6 | import json 7 | try: 8 | from urllib.parse import urlencode # Python 3 9 | except ImportError: 10 | from urllib import urlencode # Python 2 11 | 12 | from consulate import utils 13 | 14 | 15 | class Endpoint(object): 16 | """Base class for API endpoints""" 17 | 18 | KEYWORD = '' 19 | 20 | def __init__(self, uri, adapter, datacenter=None, token=None): 21 | """Create a new instance of the Endpoint class 22 | 23 | :param str uri: Base URI 24 | :param consul.adapters.Request adapter: Request adapter 25 | :param str datacenter: datacenter 26 | :param str token: Access Token 27 | 28 | """ 29 | self._adapter = adapter 30 | self._base_uri = '{0}/{1}'.format(uri, self.__class__.__name__.lower()) 31 | self._dc = datacenter 32 | self._token = token 33 | 34 | def _build_uri(self, params, query_params=None): 35 | """Build the request URI 36 | 37 | :param list params: List of path parts 38 | :param dict query_params: Build query parameters 39 | 40 | """ 41 | if not query_params: 42 | query_params = dict() 43 | if self._dc: 44 | query_params['dc'] = self._dc 45 | if self._token: 46 | query_params['token'] = self._token 47 | path = '/'.join(params) 48 | if query_params: 49 | return '{0}/{1}?{2}'.format(self._base_uri, path, 50 | urlencode(query_params)) 51 | return '{0}/{1}'.format(self._base_uri, path) 52 | 53 | def _get(self, params, query_params=None, raise_on_404=False, 54 | timeout=None): 55 | """Perform a GET request 56 | 57 | :param list params: List of path parts 58 | :param dict query_params: Build query parameters 59 | :param timeout: How long to wait on the request for 60 | :type timeout: int or float or None 61 | :rtype: dict or list or None 62 | 63 | """ 64 | response = self._adapter.get(self._build_uri(params, query_params), 65 | timeout=timeout) 66 | if utils.response_ok(response, raise_on_404): 67 | return response.body 68 | return [] 69 | 70 | def _delete( 71 | self, 72 | params, 73 | raise_on_404=False, 74 | ): 75 | """Perform a DELETE request 76 | 77 | :param list params: List of path parts 78 | :rtype: bool 79 | 80 | """ 81 | response = self._adapter.delete(self._build_uri(params)) 82 | if utils.response_ok(response, raise_on_404): 83 | return response.body 84 | return False 85 | 86 | def _get_list(self, params, query_params=None): 87 | """Return a list queried from Consul 88 | 89 | :param list params: List of path parts 90 | :param dict query_params: Build query parameters 91 | 92 | """ 93 | result = self._get(params, query_params) 94 | if isinstance(result, dict): 95 | return [result] 96 | return result 97 | 98 | def _get_stream(self, params, query_params=None): 99 | """Return a list queried from Consul 100 | 101 | :param list params: List of path parts 102 | :param dict query_params: Build query parameters 103 | :rtype: iterator 104 | 105 | """ 106 | for line in self._adapter.get_stream( 107 | self._build_uri(params, query_params)): 108 | yield line 109 | 110 | def _get_no_response_body(self, url_parts, query=None): 111 | return utils.response_ok( 112 | self._adapter.get(self._build_uri(url_parts, query))) 113 | 114 | def _get_response_body(self, url_parts, query=None): 115 | response = self._adapter.get(self._build_uri(url_parts, query)) 116 | if utils.response_ok(response): 117 | return response.body 118 | 119 | def _put_no_response_body(self, url_parts, query=None, payload=None): 120 | return utils.response_ok( 121 | self._adapter.put(self._build_uri(url_parts, query), payload)) 122 | 123 | def _put_response_body(self, url_parts, query=None, payload=None): 124 | response = self._adapter.put(self._build_uri(url_parts, query), 125 | data=payload) 126 | if utils.response_ok(response): 127 | return response.body 128 | 129 | 130 | class Response(object): 131 | """Used to process and wrap the responses from Consul. 132 | 133 | :param int status_code: HTTP Status code 134 | :param str body: The response body 135 | :param dict headers: Response headers 136 | 137 | """ 138 | status_code = None 139 | body = None 140 | headers = None 141 | 142 | def __init__(self, response): 143 | """Create a new instance of the Response class. 144 | 145 | :param requests.response response: The requests response 146 | 147 | """ 148 | self.status_code = response.status_code 149 | self.body = self._demarshal(response.content) 150 | self.headers = response.headers 151 | 152 | def _demarshal(self, body): 153 | """Demarshal the request payload. 154 | 155 | :param str body: The string response body 156 | :rtype: dict or str 157 | 158 | """ 159 | if body is None: 160 | return None 161 | if self.status_code == 200: 162 | try: 163 | if utils.PYTHON3 and isinstance(body, bytes): 164 | try: 165 | body = body.decode('utf-8') 166 | except UnicodeDecodeError: 167 | pass 168 | value = json.loads(body) 169 | except (TypeError, ValueError): 170 | return body 171 | if value is None: 172 | return None 173 | if isinstance(value, bool): 174 | return value 175 | if 'error' not in value: 176 | for row in value: 177 | if 'Value' in row: 178 | try: 179 | row['Value'] = base64.b64decode(row['Value']) 180 | if isinstance(row['Value'], bytes): 181 | try: 182 | row['Value'] = row['Value'].decode('utf-8') 183 | except UnicodeDecodeError: 184 | pass 185 | except TypeError: 186 | pass 187 | if isinstance(value, list) and len(value) == 1: 188 | return value[0] 189 | return value 190 | return body 191 | -------------------------------------------------------------------------------- /consulate/api/catalog.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul Catalog Endpoint Access 3 | 4 | """ 5 | from consulate.api import base 6 | 7 | 8 | class Catalog(base.Endpoint): 9 | """The Consul agent is the core process of Consul. The agent maintains 10 | membership information, registers services, runs checks, responds to 11 | queries and more. The agent must run on every node that is part of a 12 | Consul cluster. 13 | 14 | """ 15 | 16 | def __init__(self, uri, adapter, dc=None, token=None): 17 | super(Catalog, self).__init__(uri, adapter, dc, token) 18 | 19 | def register(self, node, address, 20 | datacenter=None, 21 | service=None, 22 | check=None, 23 | node_meta=None): 24 | """A a low level mechanism for directly registering or updating 25 | entries in the catalog. It is usually recommended to use the agent 26 | local endpoints, as they are simpler and perform anti-entropy. 27 | 28 | The behavior of the endpoint depends on what keys are provided. The 29 | endpoint requires Node and Address to be provided, while Datacenter 30 | will be defaulted to match that of the agent. If only those are 31 | provided, the endpoint will register the node with the catalog. 32 | 33 | If the Service key is provided, then the service will also be 34 | registered. If ID is not provided, it will be defaulted to Service. 35 | It is mandated that the ID be node-unique. Both Tags and Port can 36 | be omitted. 37 | 38 | If the Check key is provided, then a health check will also be 39 | registered. It is important to remember that this register API is 40 | very low level. This manipulates the health check entry, but does 41 | not setup a script or TTL to actually update the status. For that 42 | behavior, an agent local check should be setup. 43 | 44 | The CheckID can be omitted, and will default to the Name. Like 45 | before, the CheckID must be node-unique. The Notes is an opaque 46 | field that is meant to hold human readable text. If a ServiceID is 47 | provided that matches the ID of a service on that node, then the 48 | check is treated as a service level health check, instead of a node 49 | level health check. Lastly, the status must be one of "unknown", 50 | "passing", "warning", or "critical". The "unknown" status is used to 51 | indicate that the initial check has not been performed yet. 52 | 53 | It is important to note that Check does not have to be provided 54 | with Service and visa-versa. They can be provided or omitted at will. 55 | 56 | Example service dict: 57 | 58 | .. code:: python 59 | 60 | 'Service': { 61 | 'ID': 'redis1', 62 | 'Service': 'redis', 63 | 'Tags': ['master', 'v1'], 64 | 'Port': 8000, 65 | } 66 | 67 | Example check dict: 68 | 69 | .. code:: python 70 | 71 | 'Check': { 72 | 'Node': 'foobar', 73 | 'CheckID': 'service:redis1', 74 | 'Name': 'Redis health check', 75 | 'Notes': 'Script based health check', 76 | 'Status': 'passing', 77 | 'ServiceID': 'redis1' 78 | } 79 | 80 | Example node_meta dict: 81 | 82 | .. code:: python 83 | 84 | 'NodeMeta': { 85 | 'somekey': 'somevalue' 86 | } 87 | 88 | :param str node: The node name 89 | :param str address: The node address 90 | :param str datacenter: The optional node datacenter 91 | :param dict service: An optional node service 92 | :param dict check: An optional node check 93 | :param dict node_meta: Optional node metadata 94 | :rtype: bool 95 | 96 | """ 97 | payload = {'Node': node, 'Address': address} 98 | if datacenter: 99 | payload['Datacenter'] = datacenter 100 | if service: 101 | payload['Service'] = service 102 | if check: 103 | payload['Check'] = check 104 | if node_meta: 105 | payload['NodeMeta'] = node_meta 106 | 107 | return self._put_response_body(['register'], None, payload) 108 | 109 | def deregister(self, node, datacenter=None, 110 | check_id=None, service_id=None): 111 | """Directly remove entries in the catalog. It is usually recommended 112 | to use the agent local endpoints, as they are simpler and perform 113 | anti-entropy. 114 | 115 | The behavior of the endpoint depends on what keys are provided. The 116 | endpoint requires ``node`` to be provided, while ``datacenter`` will 117 | be defaulted to match that of the agent. If only ``node`` is provided, 118 | then the node, and all associated services and checks are deleted. If 119 | ``check_id`` is provided, only that check belonging to the node is 120 | removed. If ``service_id`` is provided, then the service along with 121 | it's associated health check (if any) is removed. 122 | 123 | :param str node: The node for the action 124 | :param str datacenter: The optional datacenter for the node 125 | :param str check_id: The optional check_id to remove 126 | :param str service_id: The optional service_id to remove 127 | :rtype: bool 128 | 129 | """ 130 | payload = {'Node': node} 131 | if datacenter: 132 | payload['Datacenter'] = datacenter 133 | if check_id: 134 | payload['CheckID'] = check_id 135 | if service_id: 136 | payload['ServiceID'] = service_id 137 | return self._put_response_body(['deregister'], None, payload) 138 | 139 | def datacenters(self): 140 | """Return all the datacenters that are known by the Consul server. 141 | 142 | :rtype: list 143 | 144 | """ 145 | return self._get_list(['datacenters']) 146 | 147 | def node(self, node_id): 148 | """Return the node data for the specified node 149 | 150 | :param str node_id: The node ID 151 | :rtype: dict 152 | 153 | """ 154 | return self._get(['node', node_id]) 155 | 156 | def nodes(self, node_meta=None): 157 | """Return all of the nodes for the current datacenter. 158 | 159 | :param str node_meta: Desired node metadata 160 | :rtype: list 161 | 162 | """ 163 | query_params = {'node-meta': node_meta} if node_meta else {} 164 | return self._get_list(['nodes'], query_params) 165 | 166 | def service(self, service_id): 167 | """Return the service details for the given service 168 | 169 | :param str service_id: The service id 170 | :rtype: list 171 | 172 | """ 173 | return self._get_list(['service', service_id]) 174 | 175 | def services(self): 176 | """Return a list of all of the services for the current datacenter. 177 | 178 | :rtype: list 179 | 180 | """ 181 | return self._get_list(['services']) 182 | -------------------------------------------------------------------------------- /consulate/api/coordinate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul Coordinate Endpoint Access 3 | 4 | """ 5 | from consulate.api import base 6 | from math import sqrt 7 | 8 | class Coordinate(base.Endpoint): 9 | """Used to query node coordinates. 10 | """ 11 | 12 | def node(self, node_id): 13 | """Return coordinates for the given node. 14 | 15 | :param str node_id: The node ID 16 | :rtype: dict 17 | 18 | """ 19 | return self._get(['node', node_id]) 20 | 21 | def nodes(self): 22 | """Return coordinates for the current datacenter. 23 | 24 | :rtype: list 25 | 26 | """ 27 | return self._get_list(['nodes']) 28 | 29 | def rtt(self, src, dst): 30 | """Calculated RTT between two node coordinates. 31 | 32 | :param dict src 33 | :param dict dst 34 | :rtype float 35 | 36 | """ 37 | 38 | if not isinstance(src, (dict)): 39 | raise ValueError('coordinate object must be a dictionary') 40 | if not isinstance(dst, (dict)): 41 | raise ValueError('coordinate object must be a dictionary') 42 | if 'Coord' not in src: 43 | raise ValueError('coordinate object has no Coord key') 44 | if 'Coord' not in dst: 45 | raise ValueError('coordinate object has no Coord key') 46 | 47 | src_coord = src['Coord'] 48 | dst_coord = dst['Coord'] 49 | 50 | if len(src_coord.get('Vec')) != len(dst_coord.get('Vec')): 51 | raise ValueError('coordinate objects are not compatible due to different length') 52 | 53 | sumsq = 0.0 54 | for i in xrange(len(src_coord.get('Vec'))): 55 | diff = src_coord.get('Vec')[i] - dst_coord.get('Vec')[i] 56 | sumsq += diff * diff 57 | 58 | rtt = sqrt(sumsq) + src_coord.get('Height') + dst_coord.get('Height') 59 | adjusted = rtt + src_coord.get('Adjustment') + dst_coord.get('Adjustment') 60 | if adjusted > 0.0: 61 | rtt = adjusted 62 | 63 | return rtt * 1000 64 | -------------------------------------------------------------------------------- /consulate/api/event.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul Event Endpoint Access 3 | 4 | """ 5 | from consulate.api import base 6 | 7 | 8 | class Event(base.Endpoint): 9 | """The Event endpoints are used to fire a new event and list recent events. 10 | 11 | """ 12 | 13 | def fire(self, name, 14 | payload=None, 15 | datacenter=None, 16 | node=None, 17 | service=None, 18 | tag=None): 19 | """Trigger a new user Event 20 | 21 | :param str name: The name of the event 22 | :param str payload: The opaque event payload 23 | :param str datacenter: Optional datacenter to fire the event in 24 | :param str node: Optional node to fire the event for 25 | :param str service: Optional service to fire the event for 26 | :param str tag: Option tag to fire the event for 27 | :return str: the new event ID 28 | 29 | """ 30 | query_args = {} 31 | if datacenter: 32 | query_args['dc'] = datacenter 33 | if node: 34 | query_args['node'] = node 35 | if service: 36 | query_args['service'] = service 37 | if tag: 38 | query_args['tag'] = tag 39 | response = self._adapter.put(self._build_uri(['fire', name], 40 | query_args), payload) 41 | return response.body.get('ID') 42 | 43 | def list(self, name=None): 44 | """Returns the most recent events known by the agent. As a consequence 45 | of how the event command works, each agent may have a different view of 46 | the events. Events are broadcast using the gossip protocol, so they 47 | have no global ordering nor do they make a promise of delivery. 48 | 49 | :return: list 50 | 51 | """ 52 | query_args = {} 53 | if name: 54 | query_args['name'] = name 55 | return self._get(['list'], query_args) 56 | -------------------------------------------------------------------------------- /consulate/api/health.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul Health Endpoint Access 3 | 4 | """ 5 | from consulate.api import base 6 | 7 | 8 | class Health(base.Endpoint): 9 | """Used to query health related information. It is provided separately 10 | from the Catalog, since users may prefer to not use the health checking 11 | mechanisms as they are totally optional. Additionally, some of the query 12 | results from the Health system are filtered, while the Catalog endpoints 13 | provide the raw entries. 14 | 15 | """ 16 | 17 | def checks(self, service_id, node_meta=None): 18 | """Return checks for the given service. 19 | 20 | :param str service_id: The service ID 21 | :param str node_meta: Filter checks using node metadata 22 | :rtype: list 23 | 24 | """ 25 | query_params = {'node-meta': node_meta} if node_meta else {} 26 | return self._get_list(['checks', service_id], query_params) 27 | 28 | def node(self, node_id): 29 | """Return the health info for a given node. 30 | 31 | :param str node_id: The node ID 32 | :rtype: list 33 | 34 | """ 35 | return self._get_list(['node', node_id]) 36 | 37 | def service(self, service_id, tag=None, passing=None, node_meta=None): 38 | """Returns the nodes and health info of a service 39 | 40 | :param str service_id: The service ID 41 | :param str node_meta: Filter services using node metadata 42 | :rtype: list 43 | 44 | """ 45 | 46 | query_params = {} 47 | if tag: 48 | query_params['tag'] = tag 49 | if passing: 50 | query_params['passing'] = '' 51 | if node_meta: 52 | query_params['node-meta'] = node_meta 53 | 54 | return self._get_list(['service', service_id], 55 | query_params=query_params) 56 | 57 | def state(self, state): 58 | """Returns the checks in a given state where state is one of 59 | "unknown", "passing", "warning", or "critical". 60 | 61 | :param str state: The state to get checks for 62 | :rtype: list 63 | 64 | """ 65 | return self._get_list(['state', state]) 66 | -------------------------------------------------------------------------------- /consulate/api/kv.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul KV Endpoint Access 3 | 4 | """ 5 | from consulate.api import base 6 | from consulate import utils, exceptions 7 | 8 | 9 | class KV(base.Endpoint): 10 | """The :py:class:`consul.api.KV` class implements a :py:class:`dict` like 11 | interface for working with the Key/Value service. Simply use items on the 12 | :py:class:`consulate.Session` like you would with a :py:class:`dict` to 13 | :py:meth:`get `, 14 | :py:meth:`set `, or 15 | :py:meth:`delete ` values in the key/value store. 16 | 17 | Additionally, :py:class:`KV ` acts as an 18 | :py:meth:`iterator `, providing methods to 19 | iterate over :py:meth:`keys `, 20 | :py:meth:`values `, 21 | :py:meth:`keys and values `, etc. 22 | 23 | Should you need access to get or set the flag value, the 24 | :py:meth:`get_record `, 25 | :py:meth:`set_record `, 26 | and :py:meth:`records ` provide a way to access 27 | the additional fields exposed by the KV service. 28 | 29 | """ 30 | 31 | def __contains__(self, item): 32 | """Return True if there is a value set in the Key/Value service for the 33 | given key. 34 | 35 | :param str item: The key to check for 36 | :rtype: bool 37 | 38 | """ 39 | item = item.lstrip('/') 40 | return self._get_no_response_body([item]) 41 | 42 | def __delitem__(self, item): 43 | """Delete an item from the Key/Value service 44 | 45 | :param str item: The key name 46 | 47 | """ 48 | self._delete_item(item) 49 | 50 | def __getitem__(self, item): 51 | """Get a value from the Key/Value service, returning it fully 52 | decoded if possible. 53 | 54 | :param str item: The item name 55 | :rtype: mixed 56 | :raises: KeyError 57 | 58 | """ 59 | value = self._get_item(item) 60 | if not value: 61 | raise KeyError('Key not found ({0})'.format(item)) 62 | return value.get('Value') 63 | 64 | def __iter__(self): 65 | """Iterate over all the keys in the Key/Value service 66 | 67 | :rtype: iterator 68 | 69 | """ 70 | for key in self.keys(): 71 | yield key 72 | 73 | def __len__(self): 74 | """Return the number if items in the Key/Value service 75 | 76 | :return: int 77 | 78 | """ 79 | return len(self._get_all_items()) 80 | 81 | def __setitem__(self, item, value): 82 | """Set a value in the Key/Value service, using the CAS mechanism 83 | to ensure that the set is atomic. If the value passed in is not a 84 | string, an attempt will be made to JSON encode the value prior to 85 | setting it. 86 | 87 | :param str item: The key to set 88 | :param mixed value: The value to set 89 | :raises: KeyError 90 | 91 | """ 92 | self._set_item(item, value) 93 | 94 | def acquire_lock(self, item, session, value=None, cas=None, flags=None): 95 | """Use Consul for locking by specifying the item/key to lock with 96 | and a session value for removing the lock. 97 | 98 | :param str item: The item in the Consul KV database 99 | :param str session: The session value for the lock 100 | :param mixed value: An optional value to set for the lock 101 | :param int cas: Optional Check-And-Set index value 102 | :param int flags: User defined flags to set 103 | :return: bool 104 | 105 | """ 106 | query_params = {'acquire': session} 107 | if cas is not None: 108 | query_params['cas'] = cas 109 | if flags is not None: 110 | query_params['flags'] = flags 111 | return self._put_response_body([item], query_params, value) 112 | 113 | def delete(self, item, recurse=False): 114 | """Delete an item from the Key/Value service 115 | 116 | :param str item: The item key 117 | :param bool recurse: Remove keys prefixed with the item pattern 118 | :raises: KeyError 119 | 120 | """ 121 | return self._delete_item(item, recurse) 122 | 123 | def get(self, item, default=None, raw=False): 124 | """Get a value from the Key/Value service, returning it fully 125 | decoded if possible. 126 | 127 | :param str item: The item key 128 | :param mixed default: A default value to return if the get fails 129 | :param bool raw: Return the raw value from Consul 130 | :rtype: mixed 131 | :raises: KeyError 132 | 133 | """ 134 | response = self._get_item(item, raw) 135 | if isinstance(response, dict): 136 | return response.get('Value', default) 137 | return response or default 138 | 139 | def get_record(self, item): 140 | """Get the full record from the Key/Value service, returning 141 | all fields including the flag. 142 | 143 | :param str item: The item key 144 | :rtype: dict 145 | :raises: KeyError 146 | 147 | """ 148 | return self._get_item(item) 149 | 150 | def find(self, prefix, separator=None): 151 | """Find all keys with the specified prefix, returning a dict of 152 | matches. 153 | 154 | *Example:* 155 | 156 | .. code:: python 157 | 158 | >>> consul.kv.find('b') 159 | {'baz': 'qux', 'bar': 'baz'} 160 | 161 | :param str prefix: The prefix to search with 162 | :rtype: dict 163 | 164 | """ 165 | query_params = {'recurse': None} 166 | if separator: 167 | query_params['keys'] = prefix 168 | query_params['separator'] = separator 169 | response = self._get_list([prefix.lstrip('/')], query_params) 170 | if separator: 171 | results = response 172 | else: 173 | results = {} 174 | for row in response: 175 | results[row['Key']] = row['Value'] 176 | return results 177 | 178 | def items(self): 179 | """Return a dict of all of the key/value pairs in the Key/Value service 180 | 181 | *Example:* 182 | 183 | .. code:: python 184 | 185 | >>> consul.kv.items() 186 | {'foo': 'bar', 'bar': 'baz', 'quz': True, 'corgie': 'dog'} 187 | 188 | :rtype: dict 189 | 190 | """ 191 | return [{item['Key']: item['Value']} for item in self._get_all_items()] 192 | 193 | def iteritems(self): 194 | """Iterate over the dict of key/value pairs in the Key/Value service 195 | 196 | *Example:* 197 | 198 | .. code:: python 199 | 200 | >>> for key, value in consul.kv.iteritems(): 201 | ... print(key, value) 202 | ... 203 | (u'bar', 'baz') 204 | (u'foo', 'bar') 205 | (u'quz', True) 206 | 207 | :rtype: iterator 208 | 209 | """ 210 | for item in self._get_all_items(): 211 | yield item['Key'], item['Value'] 212 | 213 | def keys(self): 214 | """Return a list of all of the keys in the Key/Value service 215 | 216 | *Example:* 217 | 218 | .. code:: python 219 | 220 | >>> consul.kv.keys() 221 | [u'bar', u'foo', u'quz'] 222 | 223 | :rtype: list 224 | 225 | """ 226 | return sorted([row['Key'] for row in self._get_all_items()]) 227 | 228 | def records(self, key=None): 229 | """Return a list of tuples for all of the records in the Key/Value 230 | service 231 | 232 | *Example:* 233 | 234 | .. code:: python 235 | 236 | >>> consul.kv.records() 237 | [(u'bar', 0, 'baz'), 238 | (u'corgie', 128, 'dog'), 239 | (u'foo', 0, 'bar'), 240 | (u'quz', 0, True)] 241 | 242 | :rtype: list of (Key, Flags, Value) 243 | 244 | """ 245 | if key: 246 | return [(item['Key'], item['Flags'], item['Value']) 247 | for item in self._get_list([key], {'recurse': None})] 248 | else: 249 | return [(item['Key'], item['Flags'], item['Value']) 250 | for item in self._get_all_items()] 251 | 252 | def release_lock(self, item, session): 253 | """Release an existing lock from the Consul KV database. 254 | 255 | :param str item: The item in the Consul KV database 256 | :param str session: The session value for the lock 257 | :return: bool 258 | 259 | """ 260 | return self._put_response_body([item], {'release': session}) 261 | 262 | def set(self, item, value): 263 | """Set a value in the Key/Value service, using the CAS mechanism 264 | to ensure that the set is atomic. If the value passed in is not a 265 | string, an attempt will be made to JSON encode the value prior to 266 | setting it. 267 | 268 | :param str item: The key to set 269 | :param mixed value: The value to set 270 | :raises: KeyError 271 | 272 | """ 273 | return self.__setitem__(item, value) 274 | 275 | def set_record(self, item, flags=0, value=None, replace=True): 276 | """Set a full record, including the item flag 277 | 278 | :param str item: The key to set 279 | :param mixed value: The value to set 280 | :param replace: If True existing value will be overwritten: 281 | 282 | """ 283 | self._set_item(item, value, flags, replace) 284 | 285 | def values(self): 286 | """Return a list of all of the values in the Key/Value service 287 | 288 | *Example:* 289 | 290 | .. code:: python 291 | 292 | >>> consul.kv.values() 293 | [True, 'bar', 'baz'] 294 | 295 | :rtype: list 296 | 297 | """ 298 | return [row['Value'] for row in self._get_all_items()] 299 | 300 | def _delete_item(self, item, recurse=False): 301 | """Remove an item from the Consul database 302 | 303 | :param str item: 304 | :param recurse: 305 | :return: 306 | """ 307 | query_params = {'recurse': True} if recurse else {} 308 | return self._adapter.delete(self._build_uri([item], query_params)) 309 | 310 | def _get_all_items(self): 311 | """Internal method to return a list of all items in the Key/Value 312 | service 313 | 314 | :rtype: list 315 | 316 | """ 317 | return self._get_list([''], {'recurse': None}) 318 | 319 | def _get_item(self, item, raw=False): 320 | """Internal method to get the full item record from the Key/Value 321 | service 322 | 323 | :param str item: The item to get 324 | :param bool raw: Return only the raw body 325 | :rtype: mixed 326 | 327 | """ 328 | item = item.lstrip('/') 329 | query_params = {'raw': True} if raw else {} 330 | response = self._adapter.get(self._build_uri([item], query_params)) 331 | if response.status_code == 200: 332 | return response.body 333 | return None 334 | 335 | def _get_modify_index(self, item, value, replace): 336 | """Get the modify index of the specified item. If replace is False 337 | and an item is found, return ``None``. If the existing value 338 | and the passed in value match, return ``None``. If no item exists in 339 | the KV database, return ``0``, otherwise return the ``ModifyIndex``. 340 | 341 | :param str item: The item to get the index for 342 | :param str value: The item to evaluate for equality 343 | :param bool replace: Should the item be replaced 344 | :rtype: int|None 345 | 346 | """ 347 | response = self._adapter.get(self._build_uri([item])) 348 | index = 0 349 | if response.status_code == 200: 350 | index = response.body.get('ModifyIndex') 351 | rvalue = response.body.get('Value') 352 | if rvalue == value: 353 | return None 354 | if not replace: 355 | return None 356 | return index 357 | 358 | @staticmethod 359 | def _prepare_value(value): 360 | """Prepare the value passed in and ensure that it is properly encoded 361 | 362 | :param mixed value: The value to prepare 363 | :rtype: bytes 364 | 365 | """ 366 | if not utils.is_string(value) or isinstance(value, bytes): 367 | return value 368 | try: 369 | if utils.PYTHON3: 370 | return value.encode('utf-8') 371 | elif isinstance(value, unicode): 372 | return value.encode('utf-8') 373 | except UnicodeDecodeError: 374 | return value 375 | return value 376 | 377 | def _set_item(self, item, value, flags=None, replace=True, 378 | query_params=None): 379 | """Internal method for setting a key/value pair with flags in the 380 | Key/Value service 381 | 382 | :param str item: The key to set 383 | :param mixed value: The value to set 384 | :param int flags: User defined flags to set 385 | :param bool replace: Overwrite existing values 386 | :raises: KeyError 387 | 388 | """ 389 | value = self._prepare_value(value) 390 | if value and item.endswith('/'): 391 | item = item.rstrip('/') 392 | 393 | index = self._get_modify_index(item, value, replace) 394 | if index is None: 395 | return True 396 | query_params = query_params or {} 397 | query_params.update({'cas': index}) 398 | if flags is not None: 399 | query_params['flags'] = flags 400 | response = self._adapter.put(self._build_uri([item], query_params), 401 | value) 402 | if not response.status_code == 200 or not response.body: 403 | if response.status_code == 500: 404 | raise exceptions.ServerError( 405 | response.body or 'Internal Consul server error') 406 | raise KeyError( 407 | 'Error setting "{0}" ({1})'.format(item, response.status_code)) 408 | -------------------------------------------------------------------------------- /consulate/api/lock.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lock Object for easy locking 3 | 4 | """ 5 | import contextlib 6 | import logging 7 | import uuid 8 | 9 | from consulate.api import base 10 | from consulate import exceptions 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class Lock(base.Endpoint): 16 | """Wrapper for easy :class:`~consulate.api.kv.KV` locks. Keys are 17 | automatically prefixed with ``consulate/locks/``. To change the prefix or 18 | remove it invoke the :meth:~consulate.api.lock.Lock.prefix` method. 19 | 20 | Example: 21 | 22 | .. code:: python 23 | 24 | import consulate 25 | 26 | consul = consulate.Consul() 27 | with consul.lock.acquire('my-key'): 28 | print('Locked: {}'.format(consul.lock.key)) 29 | # Do stuff 30 | 31 | :raises: :exc:`~consulate.exception.LockError` 32 | 33 | """ 34 | DEFAULT_PREFIX = 'consulate/locks' 35 | 36 | def __init__(self, uri, adapter, session, datacenter=None, token=None): 37 | """Create a new instance of the Lock 38 | 39 | :param str uri: Base URI 40 | :param consul.adapters.Request adapter: Request adapter 41 | :param consul.api.session.Session session: Session endpoint instance 42 | :param str datacenter: datacenter 43 | :param str token: Access Token 44 | 45 | """ 46 | super(Lock, self).__init__(uri, adapter, datacenter, token) 47 | self._base_uri = '{0}/kv'.format(uri) 48 | self._session = session 49 | self._session_id = None 50 | self._item = str(uuid.uuid4()) 51 | self._prefix = self.DEFAULT_PREFIX 52 | 53 | @contextlib.contextmanager 54 | def acquire(self, key=None, value=None): 55 | """A context manager that allows you to acquire the lock, optionally 56 | passing in a key and/or value. 57 | 58 | :param str key: The key to lock 59 | :param str value: The value to set in the lock 60 | :raises: :exc:`~consulate.exception.LockError` 61 | 62 | """ 63 | self._acquire(key, value) 64 | yield 65 | self._release() 66 | 67 | @property 68 | def key(self): 69 | """Return the lock key 70 | 71 | :rtype: str 72 | 73 | """ 74 | return self._item 75 | 76 | def prefix(self, value): 77 | """Override the path prefix for the lock key 78 | 79 | :param str value: The value to set the path prefix to 80 | 81 | """ 82 | self._prefix = value or '' 83 | 84 | def _acquire(self, key=None, value=None): 85 | self._session_id = self._session.create() 86 | self._item = '/'.join([self._prefix, (key or str(uuid.uuid4()))]) 87 | LOGGER.debug('Acquiring a lock of %s for session %s', 88 | self._item, self._session_id) 89 | response = self._put_response_body([self._item], 90 | {'acquire': self._session_id}, 91 | value) 92 | if not response: 93 | self._session.destroy(self._session_id) 94 | raise exceptions.LockFailure() 95 | 96 | def _release(self): 97 | """Release the lock""" 98 | self._put_response_body([self._item], {'release': self._session_id}) 99 | self._adapter.delete(self._build_uri([self._item])) 100 | self._session.destroy(self._session_id) 101 | self._item, self._session_id = None, None 102 | -------------------------------------------------------------------------------- /consulate/api/session.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul Session Endpoint Access 3 | 4 | """ 5 | from consulate.api import base 6 | 7 | 8 | class Session(base.Endpoint): 9 | """Create, destroy, and query Consul sessions.""" 10 | 11 | def create(self, 12 | name=None, 13 | behavior='release', 14 | node=None, 15 | delay=None, 16 | ttl=None, 17 | checks=None): 18 | """Initialize a new session. 19 | 20 | None of the fields are mandatory, and in fact no body needs to be PUT 21 | if the defaults are to be used. 22 | 23 | Name can be used to provide a human-readable name for the Session. 24 | 25 | Behavior can be set to either ``release`` or ``delete``. This controls 26 | the behavior when a session is invalidated. By default, this is 27 | release, causing any locks that are held to be released. Changing this 28 | to delete causes any locks that are held to be deleted. delete is 29 | useful for creating ephemeral key/value entries. 30 | 31 | Node must refer to a node that is already registered, if specified. 32 | By default, the agent's own node name is used. 33 | 34 | LockDelay (``delay``) can be specified as a duration string using a 35 | "s" suffix for seconds. The default is 15s. 36 | 37 | The TTL field is a duration string, and like LockDelay it can use "s" 38 | as a suffix for seconds. If specified, it must be between 10s and 39 | 3600s currently. When provided, the session is invalidated if it is 40 | not renewed before the TTL expires. See the session internals page 41 | for more documentation of this feature. 42 | 43 | Checks is used to provide a list of associated health checks. It is 44 | highly recommended that, if you override this list, you include the 45 | default "serfHealth". 46 | 47 | :param str name: A human readable session name 48 | :param str behavior: One of ``release`` or ``delete`` 49 | :param str node: A node to create the session on 50 | :param str delay: A lock delay for the session 51 | :param str ttl: The time to live for the session 52 | :param lists checks: A list of associated health checks 53 | :return str: session ID 54 | 55 | """ 56 | payload = {'name': name} if name else {} 57 | if node: 58 | payload['Node'] = node 59 | if behavior: 60 | payload['Behavior'] = behavior 61 | if delay: 62 | payload['LockDelay'] = delay 63 | if ttl: 64 | payload['TTL'] = ttl 65 | if checks: 66 | payload['Checks'] = checks 67 | return self._put_response_body(['create'], None, payload).get('ID') 68 | 69 | def destroy(self, session_id): 70 | """Destroy an existing session 71 | 72 | :param str session_id: The session to destroy 73 | :return: bool 74 | 75 | """ 76 | return self._put_no_response_body(['destroy', session_id]) 77 | 78 | def info(self, session_id): 79 | """Returns the requested session information within a given dc. 80 | By default, the dc of the agent is queried. 81 | 82 | :param str session_id: The session to get info about 83 | :return: dict 84 | 85 | """ 86 | return self._get_response_body(['info', session_id]) 87 | 88 | def list(self): 89 | """Returns the active sessions for a given dc. 90 | 91 | :return: list 92 | 93 | """ 94 | return self._get_response_body(['list']) 95 | 96 | def node(self, node): 97 | """Returns the active sessions for a given node and dc. 98 | By default, the dc of the agent is queried. 99 | 100 | :param str node: The node to get active sessions for 101 | :return: list 102 | 103 | """ 104 | return self._get_response_body(['node', node]) 105 | 106 | def renew(self, session_id): 107 | """Renew the given session. This is used with sessions that have a TTL, 108 | and it extends the expiration by the TTL. By default, the dc 109 | of the agent is queried. 110 | 111 | :param str session_id: The session to renew 112 | :return: dict 113 | 114 | """ 115 | return self._put_response_body(['renew', session_id]) 116 | -------------------------------------------------------------------------------- /consulate/api/status.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul Status Endpoint Access 3 | 4 | """ 5 | from consulate.api import base 6 | 7 | 8 | class Status(base.Endpoint): 9 | """Get information about the status of the Consul cluster. This are 10 | generally very low level, and not really useful for clients. 11 | 12 | """ 13 | 14 | def leader(self): 15 | """Get the Raft leader for the datacenter the agent is running in. 16 | 17 | :rtype: str 18 | 19 | """ 20 | return self._get(['leader']) 21 | 22 | def peers(self): 23 | """Get the Raft peers for the datacenter the agent is running in. 24 | 25 | :rtype: list 26 | 27 | """ 28 | value = self._get(['peers']) 29 | if not isinstance(value, list): 30 | return [value] 31 | return value 32 | -------------------------------------------------------------------------------- /consulate/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consul client object 3 | 4 | """ 5 | import os 6 | from consulate import adapters, api, utils 7 | 8 | DEFAULT_HOST = os.environ.get('CONSUL_HOST') or 'localhost' 9 | DEFAULT_PORT = os.environ.get('CONSUL_PORT') or 8500 10 | DEFAULT_ADDR = os.environ.get('CONSUL_HTTP_ADDR') 11 | DEFAULT_SCHEME = 'http' 12 | DEFAULT_TOKEN = os.environ.get('CONSUL_HTTP_TOKEN') 13 | API_VERSION = 'v1' 14 | 15 | 16 | class Consul(object): 17 | """Access the Consul HTTP API via Python. 18 | 19 | The default values connect to Consul via ``localhost:8500`` via http. If 20 | you want to connect to Consul via a local UNIX socket, you'll need to 21 | override both the ``scheme``, ``port`` and the ``adapter`` like so: 22 | 23 | .. code:: python 24 | 25 | consul = consulate.Consul('/path/to/socket', None, scheme='http+unix', 26 | adapter=consulate.adapters.UnixSocketRequest) 27 | services = consul.agent.services() 28 | 29 | :param str addr: The CONSUL_HTTP_ADDR if available (Default: None) 30 | :param str host: The host name to connect to (Default: localhost) 31 | :param int port: The port to connect on (Default: 8500) 32 | :param str datacenter: Specify a specific data center 33 | :param str token: Specify a ACL token to use 34 | :param str scheme: Specify the scheme (Default: http) 35 | :param class adapter: Specify to override the request adapter 36 | (Default: :py:class:`consulate.adapters.Request`) 37 | :param bool/str verify: Specify how to verify TLS certificates 38 | :param tuple cert: Specify client TLS certificate and key files 39 | :param float timeout: Timeout in seconds for API requests (Default: None) 40 | 41 | """ 42 | def __init__(self, 43 | addr=DEFAULT_ADDR, 44 | host=DEFAULT_HOST, 45 | port=DEFAULT_PORT, 46 | datacenter=None, 47 | token=DEFAULT_TOKEN, 48 | scheme=DEFAULT_SCHEME, 49 | adapter=None, 50 | verify=True, 51 | cert=None, 52 | timeout=None): 53 | """Create a new instance of the Consul class""" 54 | base_uri = self._base_uri(addr=addr, 55 | scheme=scheme, 56 | host=host, 57 | port=port) 58 | self._adapter = adapter() if adapter else adapters.Request( 59 | timeout=timeout, verify=verify, cert=cert) 60 | self._acl = api.ACL(base_uri, self._adapter, datacenter, token) 61 | self._agent = api.Agent(base_uri, self._adapter, datacenter, token) 62 | self._catalog = api.Catalog(base_uri, self._adapter, datacenter, token) 63 | self._event = api.Event(base_uri, self._adapter, datacenter, token) 64 | self._health = api.Health(base_uri, self._adapter, datacenter, token) 65 | self._coordinate = api.Coordinate(base_uri, self._adapter, datacenter, 66 | token) 67 | self._kv = api.KV(base_uri, self._adapter, datacenter, token) 68 | self._session = api.Session(base_uri, self._adapter, datacenter, token) 69 | self._status = api.Status(base_uri, self._adapter, datacenter, token) 70 | self._lock = api.Lock(base_uri, self._adapter, self._session, 71 | datacenter, token) 72 | 73 | @property 74 | def acl(self): 75 | """Access the Consul 76 | `ACL `_ API 77 | 78 | :rtype: :py:class:`consulate.api.acl.ACL` 79 | 80 | """ 81 | return self._acl 82 | 83 | @property 84 | def agent(self): 85 | """Access the Consul 86 | `Agent `_ API 87 | 88 | :rtype: :py:class:`consulate.api.agent.Agent` 89 | 90 | """ 91 | return self._agent 92 | 93 | @property 94 | def catalog(self): 95 | """Access the Consul 96 | `Catalog `_ API 97 | 98 | :rtype: :py:class:`consulate.api.catalog.Catalog` 99 | 100 | """ 101 | return self._catalog 102 | 103 | @property 104 | def event(self): 105 | """Access the Consul 106 | `Events `_ API 107 | 108 | :rtype: :py:class:`consulate.api.event.Event` 109 | 110 | """ 111 | return self._event 112 | 113 | @property 114 | def health(self): 115 | """Access the Consul 116 | `Health `_ API 117 | 118 | :rtype: :py:class:`consulate.api.health.Health` 119 | 120 | """ 121 | return self._health 122 | 123 | @property 124 | def coordinate(self): 125 | """Access the Consul 126 | `Coordinate `_ API 127 | 128 | :rtype: :py:class:`consulate.api.coordinate.Coordinate` 129 | 130 | """ 131 | return self._coordinate 132 | 133 | @property 134 | def kv(self): 135 | """Access the Consul 136 | `KV `_ API 137 | 138 | :rtype: :py:class:`consulate.api.kv.KV` 139 | 140 | """ 141 | return self._kv 142 | 143 | @property 144 | def lock(self): 145 | """Wrapper for easy :class:`~consulate.api.kv.KV` locks. 146 | `Semaphore ` _Guide 147 | Example: 148 | 149 | .. code:: python 150 | 151 | import consulate 152 | 153 | consul = consulate.Consul() 154 | with consul.lock.acquire('my-key'): 155 | print('Locked: {}'.format(consul.lock.key)) 156 | # Do stuff 157 | 158 | :rtype: :class:`~consulate.api.lock.Lock` 159 | 160 | """ 161 | return self._lock 162 | 163 | @property 164 | def session(self): 165 | """Access the Consul 166 | `Session `_ API 167 | 168 | :rtype: :py:class:`consulate.api.session.Session` 169 | 170 | """ 171 | return self._session 172 | 173 | @property 174 | def status(self): 175 | """Access the Consul 176 | `Status `_ API 177 | 178 | :rtype: :py:class:`consulate.api.status.Status` 179 | 180 | """ 181 | return self._status 182 | 183 | @staticmethod 184 | def _base_uri(scheme, host, port, addr=None): 185 | """Return the base URI to use for API requests. Set ``port`` to None 186 | when creating a UNIX Socket URL. 187 | 188 | :param str scheme: The scheme to use (Default: http) 189 | :param str host: The host name to connect to (Default: localhost) 190 | :param int|None port: The port to connect on (Default: 8500) 191 | :rtype: str 192 | 193 | """ 194 | if addr is None: 195 | if port: 196 | return '{0}://{1}:{2}/{3}'.format(scheme, host, port, 197 | API_VERSION) 198 | return '{0}://{1}/{2}'.format(scheme, utils.quote(host, ''), 199 | API_VERSION) 200 | return '{0}/{1}'.format(addr, API_VERSION) 201 | -------------------------------------------------------------------------------- /consulate/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Consulate Exceptions 3 | 4 | """ 5 | 6 | 7 | class ConsulateException(Exception): 8 | """Base Consul exception""" 9 | 10 | 11 | class RequestError(ConsulateException): 12 | """There was an error making the request to the consul server""" 13 | 14 | 15 | class ClientError(ConsulateException): 16 | """There was an error in the request that was made to consul""" 17 | 18 | 19 | class ServerError(ConsulateException): 20 | """An internal Consul server error occurred""" 21 | 22 | 23 | class ACLDisabled(ConsulateException): 24 | """Raised when ACL related calls are made while ACLs are disabled""" 25 | 26 | 27 | class ACLFormatError(ConsulateException): 28 | """Raised when PolicyLinks is missing 'ID' and 'Name' in a PolicyLink or 29 | when ServiceIdentities is missing 'ServiceName' field in a ServiceIdentity. 30 | 31 | """ 32 | 33 | 34 | class Forbidden(ConsulateException): 35 | """Raised when ACLs are enabled and the token does not validate""" 36 | 37 | 38 | class NotFound(ConsulateException): 39 | """Raised when an operation is attempted with a value that can not be 40 | found. 41 | 42 | """ 43 | 44 | 45 | class LockFailure(ConsulateException): 46 | """Raised by :class:`~consulate.api.lock.Lock` if the lock can not be 47 | acquired. 48 | 49 | """ 50 | -------------------------------------------------------------------------------- /consulate/models/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Consulate Data Models""" 3 | -------------------------------------------------------------------------------- /consulate/models/acl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Models for the ACL endpoints""" 3 | import uuid 4 | 5 | from consulate.models import base 6 | 7 | 8 | def _validate_link_array(value, model): 9 | """ Validate the policies or roles links are formatted correctly. 10 | 11 | :param list value: An array of PolicyLink or RoleLink. 12 | :param rtype: bool 13 | 14 | """ 15 | return all(['ID' in link or 'Name' in link for link in value]) 16 | 17 | 18 | def _validate_service_identities(value, model): 19 | """ Validate service_identities is formatted correctly. 20 | 21 | :param ServiceIdentities value: A ServiceIdentity list 22 | :param rtype: bool 23 | 24 | """ 25 | return all( 26 | ['ServiceName' in service_identity for service_identity in value]) 27 | 28 | 29 | class ACLPolicy(base.Model): 30 | """Defines the model used for an ACL policy.""" 31 | __slots__ = ['datacenters', 'description', 'id', 'name', 'rules'] 32 | 33 | __attributes__ = { 34 | 'datacenters': { 35 | 'key': 'Datacenters', 36 | 'type': list, 37 | }, 38 | 'description': { 39 | 'key': 'Description', 40 | 'type': str, 41 | }, 42 | 'id': { 43 | 'key': 'ID', 44 | 'type': uuid.UUID, 45 | 'cast_from': str, 46 | 'cast_to': str, 47 | }, 48 | 'name': { 49 | 'key': 'Name', 50 | 'type': str, 51 | }, 52 | 'rules': { 53 | 'key': 'Rules', 54 | 'type': str, 55 | } 56 | } 57 | 58 | 59 | class ACLRole(base.Model): 60 | """Defines the model used for an ACL role.""" 61 | __slots__ = ['description', 'name', 'policies', 'service_identities'] 62 | 63 | __attributes__ = { 64 | 'description': { 65 | 'key': 'Description', 66 | 'type': str, 67 | }, 68 | 'name': { 69 | 'key': 'Name', 70 | 'type': str, 71 | 'required': True, 72 | }, 73 | 'policies': { 74 | 'key': 'Policies', 75 | 'type': list, 76 | 'validator': _validate_link_array, 77 | }, 78 | "service_identities": { 79 | 'key': 'ServiceIdentities', 80 | 'type': list, 81 | 'validator': _validate_service_identities, 82 | } 83 | } 84 | 85 | 86 | class ACLToken(base.Model): 87 | """Defines the model used for an ACL token.""" 88 | __slots__ = [ 89 | 'accessor_id', 'description', 'expiration_time', 'expiration_ttl', 90 | 'local', 'policies', 'roles', 'secret_id', 'service_identities' 91 | ] 92 | 93 | __attributes__ = { 94 | 'accessor_id': { 95 | 'key': 'AccessorID', 96 | 'type': uuid.UUID, 97 | 'cast_from': str, 98 | 'cast_to': str, 99 | }, 100 | 'description': { 101 | 'key': 'Description', 102 | 'type': str, 103 | }, 104 | 'expiration_time': { 105 | 'key': 'ExpirationTime', 106 | 'type': str, 107 | }, 108 | 'expiration_ttl': { 109 | 'key': 'ExpirationTTL', 110 | 'type': str, 111 | }, 112 | 'local': { 113 | 'key': 'Local', 114 | 'type': bool, 115 | }, 116 | 'policies': { 117 | 'key': 'Policies', 118 | 'type': list, 119 | 'validator': _validate_link_array, 120 | }, 121 | 'roles': { 122 | 'key': 'Roles', 123 | 'type': list, 124 | 'validator': _validate_link_array, 125 | }, 126 | 'secret_id': { 127 | 'key': 'SecretID', 128 | 'type': uuid.UUID, 129 | 'cast_from': str, 130 | 'cast_to': str, 131 | }, 132 | "service_identities": { 133 | 'key': 'ServiceIdentities', 134 | 'type': list, 135 | 'validator': _validate_service_identities, 136 | } 137 | } 138 | 139 | 140 | # NOTE: Everything below here is deprecated post consul-1.4.0. 141 | 142 | 143 | class ACL(base.Model): 144 | """Defines the model used for an individual ACL token.""" 145 | __slots__ = ['id', 'name', 'type', 'rules'] 146 | 147 | __attributes__ = { 148 | 'id': { 149 | 'key': 'ID', 150 | 'type': uuid.UUID, 151 | 'cast_from': str, 152 | 'cast_to': str 153 | }, 154 | 'name': { 155 | 'key': 'Name', 156 | 'type': str 157 | }, 158 | 'type': { 159 | 'key': 'Type', 160 | 'type': str, 161 | 'enum': {'client', 'management'}, 162 | 'required': True 163 | }, 164 | 'rules': { 165 | 'key': 'Rules', 166 | 'type': str 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /consulate/models/agent.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Models for the Agent endpoints""" 3 | from consulate.models import base 4 | from consulate import utils 5 | 6 | 7 | def _validate_args(value, model): 8 | """Validate that the args values are all strings and that it does not 9 | conflict with other attributes. 10 | 11 | :param list([str]) value: The args value 12 | :param consulate.models.agent.Check model: The model instance. 13 | :rtype: bool 14 | 15 | """ 16 | return all([isinstance(v, str) for v in value]) \ 17 | and not model.args and not model.grpc and not model.http \ 18 | and not model.ttl 19 | 20 | 21 | def _validate_grpc(value, model): 22 | """Validate that the HTTP value is a URL and that it does not conflict 23 | with other attributes. 24 | 25 | :param str value: The URL value 26 | :param consulate.models.agent.Check model: The model instance. 27 | :rtype: bool 28 | 29 | """ 30 | return utils.validate_url(value) \ 31 | and not model.args and not model.http \ 32 | and not model.tcp and not model.ttl 33 | 34 | 35 | def _validate_http(value, model): 36 | """Validate that the HTTP value is a URL and that it does not conflict 37 | with other attributes. 38 | 39 | :param str value: The URL value 40 | :param consulate.models.agent.Check model: The model instance. 41 | :rtype: bool 42 | 43 | """ 44 | return utils.validate_url(value) \ 45 | and not model.args and not model.grpc and not model.tcp \ 46 | and not model.ttl 47 | 48 | 49 | def _validate_interval(value, model): 50 | """Validate that interval does not conflict with other attributes. 51 | 52 | :param str value: The interval value 53 | :param consulate.models.agent.Check model: The model instance. 54 | :rtype: bool 55 | 56 | """ 57 | return utils.validate_go_interval(value) and not model.ttl 58 | 59 | 60 | def _validate_tcp(_value, model): 61 | """Validate that the TCP does not conflict with other attributes. 62 | 63 | :param str _value: The TCP value 64 | :param consulate.models.agent.Check model: The model instance. 65 | :rtype: bool 66 | 67 | """ 68 | return not model.args and not model.grpc \ 69 | and not model.http and not model.ttl 70 | 71 | 72 | def _validate_ttl(value, model): 73 | """Validate that the TTL does not conflict with other attributes. 74 | 75 | :param str value: The TTL value 76 | :param consulate.models.agent.Check model: The model instance. 77 | :rtype: bool 78 | 79 | """ 80 | return utils.validate_go_interval(value) and not model.args \ 81 | and not model.grpc and not model.http \ 82 | and not model.tcp and not model.interval 83 | 84 | 85 | class Check(base.Model): 86 | """Model for making Check API requests to Consul.""" 87 | 88 | __slots__ = ['id', 'name', 'interval', 'notes', 89 | 'deregister_critical_service_after', 'args', 90 | 'docker_container_id', 'grpc', 'grpc_use_tls', 91 | 'http', 'method', 'header', 'timeout', 'tls_skip_verify', 92 | 'tcp', 'ttl', 'service_id', 'status'] 93 | 94 | __attributes__ = { 95 | 'id': { 96 | 'key': 'ID', 97 | 'type': str 98 | }, 99 | 'name': { 100 | 'key': 'Name', 101 | 'type': str, 102 | 'required': True 103 | }, 104 | 'interval': { 105 | 'key': 'Interval', 106 | 'type': str, 107 | 'validator': _validate_interval 108 | }, 109 | 'notes': { 110 | 'key': 'Notes', 111 | 'type': str 112 | }, 113 | 'deregister_critical_service_after': { 114 | 'key': 'DeregisterCriticalServiceAfter', 115 | 'type': str, 116 | 'validator': utils.validate_go_interval 117 | }, 118 | 'args': { 119 | 'key': 'Args', 120 | 'type': list, 121 | 'validator': _validate_args 122 | }, 123 | 'docker_container_id': { 124 | 'key': 'DockerContainerID', 125 | 'type': str 126 | }, 127 | 'grpc': { 128 | 'key': 'GRPC', 129 | 'type': str, 130 | 'validator': _validate_grpc 131 | }, 132 | 'grpc_use_tls': { 133 | 'key': 'GRPCUseTLS', 134 | 'type': bool 135 | }, 136 | 'http': { 137 | 'key': 'HTTP', 138 | 'type': str, 139 | 'validator': _validate_http 140 | }, 141 | 'method': { 142 | 'key': 'Method', 143 | 'type': str, 144 | 'enum': { 145 | 'HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'TRACE' 146 | } 147 | }, 148 | 'header': { 149 | 'key': 'Header', 150 | 'type': dict, 151 | 'validator': lambda h, _m: all( 152 | [(isinstance(k, str) and isinstance(v, str)) 153 | for k, v in h.items()]), 154 | 'cast_to': lambda h: {k: [v] for k, v in h.items()} 155 | }, 156 | 'timeout': { 157 | 'key': 'Timeout', 158 | 'type': str, 159 | 'validator': utils.validate_go_interval 160 | }, 161 | 'tls_skip_verify': { 162 | 'key': 'TLSSkipVerify', 163 | 'type': bool 164 | }, 165 | 'tcp': { 166 | 'key': 'TCP', 167 | 'type': str, 168 | 'validator': _validate_tcp 169 | }, 170 | 'ttl': { 171 | 'key': 'TTL', 172 | 'type': str, 173 | 'validator': _validate_ttl 174 | }, 175 | 'service_id': { 176 | 'key': 'ServiceID', 177 | 'type': str 178 | }, 179 | 'status': { 180 | 'key': 'Status', 181 | 'type': str, 182 | 'enum': {'passing', 'warning', 'critical', 'maintenance'} 183 | } 184 | } 185 | 186 | def __init__(self, **kwargs): 187 | super(Check, self).__init__(**kwargs) 188 | if (self.args or self.grpc or self.http or self.tcp) \ 189 | and not self.interval: 190 | raise ValueError('"interval" must be specified when specifying ' 191 | 'args, grpc, http, or tcp.') 192 | 193 | 194 | class Service(base.Model): 195 | """Model for making Check API requests to Consul.""" 196 | 197 | __slots__ = ['id', 'name', 'tags', 'meta', 'address', 'port', 'check', 198 | 'checks', 'enable_tag_override'] 199 | 200 | __attributes__ = { 201 | 'id': { 202 | 'key': 'ID', 203 | 'type': str 204 | }, 205 | 'name': { 206 | 'key': 'Name', 207 | 'type': str, 208 | 'required': True 209 | }, 210 | 'tags': { 211 | 'key': 'Tags', 212 | 'type': list, 213 | 'validator': lambda t, _m: all([isinstance(v, str) for v in t]) 214 | }, 215 | 'meta': { 216 | 'key': 'Meta', 217 | 'type': dict, 218 | 'validator': lambda h, _m: all( 219 | [(isinstance(k, str) and isinstance(v, str)) 220 | for k, v in h.items()]), 221 | }, 222 | 'address': { 223 | 'key': 'Address', 224 | 'type': str 225 | }, 226 | 'port': { 227 | 'key': 'Port', 228 | 'type': int 229 | }, 230 | 'check': { 231 | 'key': 'Check', 232 | 'type': Check, 233 | 'cast_to': dict 234 | }, 235 | 'checks': { 236 | 'key': 'Checks', 237 | 'type': list, 238 | 'validator': lambda c, _m: all([isinstance(v, Check) for v in c]), 239 | 'cast_to': lambda c: [dict(check) for check in c] 240 | }, 241 | 'enable_tag_override': { 242 | 'Key': 'EnableTagOverride', 243 | 'type': bool 244 | } 245 | } 246 | 247 | -------------------------------------------------------------------------------- /consulate/models/base.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Base Model 4 | 5 | """ 6 | import collections 7 | 8 | 9 | class Model(collections.Iterable): 10 | """A model contains an __attribute__ map that defines the name, 11 | its type for type validation, an optional validation method, a method 12 | used to 13 | 14 | .. python:: 15 | 16 | class MyModel(Model): 17 | 18 | __attributes__ = { 19 | 'ID': { 20 | 'type': uuid.UUID, 21 | 'required': False, 22 | 'default': None, 23 | 'cast_from': str, 24 | 'cast_to': str 25 | }, 26 | 'Serial': { 27 | 'type': int 28 | 'required': True, 29 | 'default': 0, 30 | 'validator': lambda v: v >= 0 end, 31 | } 32 | } 33 | 34 | """ 35 | 36 | __attributes__ = {} 37 | """The attributes that define the data elements of the model""" 38 | 39 | def __init__(self, **kwargs): 40 | super(Model, self).__init__() 41 | [setattr(self, name, value) for name, value in kwargs.items()] 42 | [self._set_default(name) for name in self.__attributes__.keys() 43 | if name not in kwargs.keys()] 44 | 45 | def __iter__(self): 46 | """Iterate through the model's key, value pairs. 47 | 48 | :rtype: iterator 49 | 50 | """ 51 | for name in self.__attributes__.keys(): 52 | value = self._maybe_cast_value(name) 53 | if value is not None: 54 | yield self._maybe_return_key(name), value 55 | 56 | def __setattr__(self, name, value): 57 | """Set the value for an attribute of the model, validating the 58 | attribute name and its type if the type is defined in ``__types__``. 59 | 60 | :param str name: The attribute name 61 | :param mixed value: The value to set 62 | :raises: AttributeError 63 | :raises: TypeError 64 | :raises: ValueError 65 | 66 | """ 67 | if name not in self.__attributes__: 68 | raise AttributeError('Invalid attribute "{}"'.format(name)) 69 | value = self._validate_value(name, value) 70 | super(Model, self).__setattr__(name, value) 71 | 72 | def __getattribute__(self, name): 73 | """Return the attribute from the model if it is set, otherwise 74 | returning the default if one is set. 75 | 76 | :param str name: The attribute name 77 | :rtype: mixed 78 | 79 | """ 80 | try: 81 | return super(Model, self).__getattribute__(name) 82 | except AttributeError: 83 | if name in self.__attributes__: 84 | return self.__attributes__[name].get('default', None) 85 | raise 86 | 87 | def _maybe_cast_value(self, name): 88 | """Return the attribute value, possibly cast to a different type if 89 | the ``cast_to`` item is set in the attribute definition. 90 | 91 | :param str name: The attribute name 92 | :rtype: mixed 93 | 94 | """ 95 | value = getattr(self, name) 96 | if value is not None and self.__attributes__[name].get('cast_to'): 97 | return self.__attributes__[name]['cast_to'](value) 98 | return value 99 | 100 | def _maybe_return_key(self, name): 101 | """Return the attribute name as specified in it's ``key`` definition, 102 | if specified. This is to map python attribute names to their Consul 103 | alternatives. 104 | 105 | :param str name: The attribute name 106 | :rtype: mixed 107 | 108 | """ 109 | if self.__attributes__[name].get('key'): 110 | return self.__attributes__[name]['key'] 111 | return name 112 | 113 | def _required_attr(self, name): 114 | """Returns :data:`True` if the attribute is required. 115 | 116 | :param str name: The attribute name 117 | :rtype: bool 118 | 119 | """ 120 | return self.__attributes__[name].get('required', False) 121 | 122 | def _set_default(self, name): 123 | """Set the default value for the attribute name. 124 | 125 | :param str name: The attribute name 126 | 127 | """ 128 | setattr(self, name, self.__attributes__[name].get('default', None)) 129 | 130 | def _validate_value(self, name, value): 131 | """Ensures the the value validates based upon the type or a validation 132 | function, raising an error if it does not. 133 | 134 | :param str name: The attribute name 135 | :param mixed value: The value that is being set 136 | :rtype: mixed 137 | :raises: TypeError 138 | :raises: ValueError 139 | 140 | """ 141 | if value is None: 142 | if self._required_attr(name): 143 | raise ValueError('Attribute "{}" is required'.format(name)) 144 | return 145 | 146 | if not isinstance(value, self.__attributes__[name].get('type')): 147 | cast_from = self.__attributes__[name].get('cast_from') 148 | if cast_from and isinstance(value, cast_from): 149 | value = self.__attributes__[name]['type'](value) 150 | else: 151 | raise TypeError( 152 | 'Attribute "{}" must be of type {} not {}'.format( 153 | name, self.__attributes__[name]['type'].__name__, 154 | value.__class__.__name__)) 155 | 156 | if self.__attributes__[name].get('enum') \ 157 | and value not in self.__attributes__[name]['enum']: 158 | raise ValueError( 159 | 'Attribute "{}" value {!r} not valid'.format(name, value)) 160 | 161 | validator = self.__attributes__[name].get('validator') 162 | if callable(validator): 163 | if not validator(value, self): 164 | raise ValueError( 165 | 'Attribute "{}" value {!r} did not validate'.format( 166 | name, value)) 167 | return value 168 | -------------------------------------------------------------------------------- /consulate/utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Misc utility functions and constants 4 | 5 | """ 6 | import re 7 | import sys 8 | try: # pylint: disable=import-error 9 | from urllib.parse import quote 10 | except ImportError: 11 | from urllib import quote 12 | 13 | try: # pylint: disable=import-error 14 | from urllib import parse as _urlparse 15 | except ImportError: 16 | import urlparse as _urlparse 17 | 18 | 19 | from consulate import exceptions 20 | 21 | DURATION_PATTERN = re.compile(r'^(?:(?:-|)(?:\d+|\d+\.\d+)(?:µs|ms|s|m|h))+$') 22 | PYTHON3 = True if sys.version_info > (3, 0, 0) else False 23 | 24 | 25 | def is_string(value): 26 | """Python 2 & 3 safe way to check if a value is either an instance of str 27 | or unicode. 28 | 29 | :param mixed value: The value to evaluate 30 | :rtype: bool 31 | 32 | """ 33 | checks = [isinstance(value, t) for t in [bytes, str]] 34 | if not PYTHON3: 35 | checks.append(isinstance(value, unicode)) 36 | return any(checks) 37 | 38 | 39 | def maybe_encode(value): 40 | """If the value passed in is a str, encode it as UTF-8 bytes for Python 3 41 | 42 | :param str|bytes value: The value to maybe encode 43 | :rtype: bytes 44 | 45 | """ 46 | try: 47 | return value.encode('utf-8') 48 | except AttributeError: 49 | return value 50 | 51 | 52 | def _response_error(response): 53 | """Return the decoded response error or status code if no content exists. 54 | 55 | :param requests.response response: The HTTP response 56 | :rtype: str 57 | 58 | """ 59 | return (response.body.decode('utf-8') 60 | if hasattr(response, 'body') and response.body 61 | else str(response.status_code)) 62 | 63 | 64 | def response_ok(response, raise_on_404=False): 65 | """Evaluate the HTTP response and raise the appropriate exception if 66 | required. 67 | 68 | :param requests.response response: The HTTP response 69 | :param bool raise_on_404: Raise an exception on 404 error 70 | :rtype: bool 71 | :raises: consulate.exceptions.ConsulateException 72 | 73 | """ 74 | if response.status_code == 200: 75 | return True 76 | elif response.status_code == 400: 77 | raise exceptions.ClientError(_response_error(response)) 78 | elif response.status_code == 401: 79 | raise exceptions.ACLDisabled(_response_error(response)) 80 | elif response.status_code == 403: 81 | raise exceptions.Forbidden(_response_error(response)) 82 | elif response.status_code == 404 and raise_on_404: 83 | raise exceptions.NotFound(_response_error(response)) 84 | elif response.status_code == 500: 85 | raise exceptions.ServerError(_response_error(response)) 86 | return False 87 | 88 | 89 | def validate_go_interval(value, _model=None): 90 | """Validate the value passed in returning :data:`True` if it is a Go 91 | Duration value. 92 | 93 | :param str value: The string to check 94 | :param consulate.models.base.Model _model: Optional model passed in 95 | :rtype: bool 96 | 97 | """ 98 | return DURATION_PATTERN.match(value) is not None 99 | 100 | 101 | def validate_url(value, _model=None): 102 | """Validate that the value passed in is a URL, returning :data:`True` if 103 | it is. 104 | 105 | :param str value: The string to check 106 | :param consulate.models.base.Model _model: Optional model passed in 107 | :rtype: bool 108 | 109 | """ 110 | parsed = _urlparse.urlparse(value) 111 | return parsed.scheme and parsed.netloc 112 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | consul: 4 | image: consul:1.6.0 5 | ports: 6 | - 8500 7 | volumes: 8 | - ./testing:/consul/config 9 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/consulate.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/consulate.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/consulate" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/consulate" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/acl.rst: -------------------------------------------------------------------------------- 1 | ACL 2 | === 3 | 4 | .. autoclass:: consulate.api.acl.ACL 5 | :members: 6 | :special-members: 7 | -------------------------------------------------------------------------------- /docs/agent.rst: -------------------------------------------------------------------------------- 1 | Agent 2 | ===== 3 | 4 | .. autoclass:: consulate.api.agent.Agent 5 | :members: 6 | :special-members: 7 | 8 | .. autoclass:: consulate.models.agent.Check 9 | :members: 10 | :special-members: 11 | 12 | .. autoclass:: consulate.models.agent.Service 13 | :members: 14 | :special-members: 15 | -------------------------------------------------------------------------------- /docs/catalog.rst: -------------------------------------------------------------------------------- 1 | Catalog 2 | ======= 3 | 4 | .. autoclass:: consulate.api.catalog.Catalog 5 | :members: 6 | :special-members: 7 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | sys.path.insert(0, '../') 4 | from consulate import __version__ 5 | needs_sphinx = '1.0' 6 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 7 | templates_path = [] 8 | source_suffix = '.rst' 9 | master_doc = 'index' 10 | project = 'consulate' 11 | copyright = '2014, Gavin M. Roy' 12 | version = '.'.join(__version__.split('.')[0:1]) 13 | release = __version__ 14 | intersphinx_mapping = {'python': ('https://docs.python.org/2/', None)} 15 | -------------------------------------------------------------------------------- /docs/consul.rst: -------------------------------------------------------------------------------- 1 | Consul 2 | ====== 3 | The :py:class:`consulate.Consul` class is core interface for interacting with 4 | all parts of the `Consul `_ API. 5 | 6 | Usage Examples 7 | -------------- 8 | Here is an example where the initial :py:class:`consulate.Consul` is created, 9 | connecting to Consul at ``localhost`` on port ``8500``. Once connected, a list 10 | of all service checks is returned. 11 | 12 | .. code:: python 13 | 14 | import consulate 15 | 16 | # Create a new instance of a consulate session 17 | session = consulate.Consul() 18 | 19 | # Get all of the service checks for the local agent 20 | checks = session.agent.checks() 21 | 22 | This next example creates a new :py:class:`Consul ` passing 23 | in an authorization token and then sets a key in the Consul KV service: 24 | 25 | .. code:: python 26 | 27 | import consulate 28 | 29 | session = consulate.Consul(token='5d24c96b4f6a4aefb99602ce9b60d16b') 30 | 31 | # Set the key named release_flag to True 32 | session.kv['release_flag'] = True 33 | 34 | API 35 | --- 36 | 37 | .. autoclass:: consulate.Consul 38 | :members: 39 | 40 | -------------------------------------------------------------------------------- /docs/coordinate.rst: -------------------------------------------------------------------------------- 1 | Coordinate 2 | ========== 3 | 4 | The :py:class:`Coordinate ` class provides 5 | access to Consul's Network Tomography. 6 | 7 | .. autoclass:: consulate.api.coordinate.Coordinate 8 | :members: 9 | :special-members: 10 | 11 | Usage 12 | ----- 13 | 14 | This code fetches the coordinates for the nodes in ``ny1`` cluster, and then 15 | calculates the RTT between two random nodes. 16 | 17 | .. code:: python 18 | 19 | import consulate 20 | 21 | # Create a new instance of a consulate session 22 | session = consulate.Consul() 23 | 24 | # Get coordinates for all notes in ny1 cluster 25 | coordinates = session.coordinate.nodes('ny1') 26 | 27 | # Calculate RTT between two nodes 28 | session.coordinate.rtt(coordinates[0], coordinates[1]) 29 | -------------------------------------------------------------------------------- /docs/events.rst: -------------------------------------------------------------------------------- 1 | Event 2 | ===== 3 | 4 | .. autoclass:: consulate.api.event.Event 5 | :members: 6 | :special-members: 7 | -------------------------------------------------------------------------------- /docs/health.rst: -------------------------------------------------------------------------------- 1 | Health 2 | ====== 3 | 4 | .. autoclass:: consulate.api.health.Health 5 | :members: 6 | :special-members: 7 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | Version History 2 | =============== 3 | - 1.0.0 4 | - Breaking Changes 5 | - Removed support for Python 2.6 which has been EOLed since 2013 6 | - Removed the deprecated (since 0.3) `consulate.Session` handle 7 | - Changed :meth:`~consulate.Consul.agent.check.register` to match the new API in Consul 8 | - Changed :meth:`~consulate.Consul.agent.checks` to return a :data:`dict` instead of a :data:`list`. 9 | - Changed :meth:`~consulate.Consul.agent.services` to return a :data:`dict` instead of a :data:`list`. 10 | - Changed :meth:`~consulate.Consul.agent.service.register` to match the new API in Consul and checks are now passed 11 | in as :class:`consulate.models.agent.Check` instances. 12 | - Other Changes: 13 | - Added :meth:`~consulate.Consul.agent.maintenance`, :meth:`~consulate.Consul.agent.metrics`, 14 | :meth:`~consulate.Consul.agent.monitor`, :meth:`~consulate.Consul.agent.reload`, 15 | :meth:`~consulate.Consul.agent.self`, and :meth:`~consulate.Consul.agent.token` 16 | - Added :meth:`~consulate.Consul.acl.bootstrap` and :meth:`~consulate.Consul.acl.replication` 17 | - Added :meth:`~consulate.Consul.agent.service.maintenance` (#107) - `Dj _` 18 | - Fixed run_once wrong args + subprocess parsing (#65) - Anthony Scalisi 19 | - Fixed :meth:`~consulate.Consul.catalog.register` and :meth:`~consulate.Consul.catalog.deregister` (#59) 20 | - Add support for ``flags``, ``cas``, and ``value`` in :meth:`Consulate.kv.acquire_lock` (#63) 21 | - Add ``--pretty`` option to kv backup (#69) - Brian Clark 22 | - Don't try to b64decode null values on kv restore (#68, #70) - Brian Clark 23 | - Raise server-error exception when setting a key fails due to a server error (#67) - Fredric Newberg 24 | - Address Python 2.6 incompatibility with the consulate cli and null data (#62, #61) - Wayne Walker 25 | - Added :class:`~consulate.api.lock.Lock` class for easier lock acquisition 26 | - New CLI feature to backup and restore ACLs (#71) 27 | - Added support for node metadata in :class:`consulate.Consul.api.catalog` & :class:`~consulate.Comsul.api.health` 28 | 29 | - 0.6.0 - released *2015-07-22* 30 | - Added --recurse and --trim to cli kv_get (#58) - Matt Walker 31 | - Add run-once functionality to CLI (#57) - Harrison Dahme 32 | - Fix cli kv ls -l to report empty key lengths as 0 (#55) - Matt Walker 33 | - Add ability to restore from API output (#53) - Morgan Delagrange 34 | - If specified, use CONSUL_RPC_ADDR as defaults for API scheme/host/port in CLI app (#50) - Mike Dougherty 35 | - Fix a recursion introduced in 0.5.0 with catalog.register (#49) 36 | - Unix socket support moved to extras install, no longer required (#48) - Anders Daljord Morken 37 | - Add support for HTTP health checks and CLI support for deregistering services (#47) - Anders Daljord Morken 38 | - Handle an edge case where argparse doesn't properly pass int values (#45) 39 | - Handle binary data properly (#41) 40 | - Add --base64 flag to kv backup/restore for backing up and restoring binary data (#41) 41 | - Fix status.peers() returning string instead of list if only one peer exists (#39) 42 | - Remove print debugging on error message (#37) - Christian Kauhaus 43 | - Added additional test coverage 44 | - Expose consulate.exceptions.* at consulate package level 45 | - consulate.exceptions.ACLForbidden renamed to consulate.exceptions.Forbidden 46 | - Fix content encoding issues with Python 3 47 | - 0.5.1 - released *2015-05-13* 48 | - Fix a regression with consualte cli introduced with UnixSockets (#36) - Dan Tracy 49 | - 0.5.0 - released *2015-05-13* 50 | - Add ability to talk to Consul via Unix Socket 51 | - Remove the automatic JSON deserialization attempt of KV values 52 | - Add timeout parameter when creating the consulate.Consul instance (#31) - Grzegorz Śliwiński 53 | - Add ability to specify a different request adapter when creating a consulate.Consul instance (#30) 54 | - Add a flag that will prevent consulate.KV.set_record from replacing a pre-existing value (#29) - Jakub Wierzbowski 55 | - Add a flag to the consulate cli for the restore command to prevent the replacement of pre-existing values (#29) - Jakub Wierzbowski 56 | - Add query args to consulate.Health.service (#27) - Chen Lei 57 | - Removed the ability to override the datacenter in consulate.Session APIs 58 | - Address UTF-8 decoding/encoding issues with Python 3 59 | - Remove optional simplejson use 60 | - Remove default value arg for consulate.KV.get_record 61 | - General code cleanup and reduction of duplicate code 62 | - 0.4.0 - released *2015-03-14* 63 | - Major internal restructure and code cleanup 64 | - consulate.Session renamed to consulate.Consul 65 | - Fix issues regarding UTF-8 values 66 | - Fix usage of CAS for KV.set (#15) 67 | - Added new ``consulate`` kv options: ls, mkdir, rm (#16) 68 | - Add support for KV.get raw 69 | - Add ACL endpoint support 70 | - Add Session endpoint support 71 | - Add Event endpoint support 72 | - Added KV lock support (acquire, release) 73 | - Remove all remaining fragments of Tornado support 74 | - 0.3.0 - released *2015-03-03* 75 | - Fix issues with quoting and UTF-8 in ``consulate kv backup/restore`` (#6, #8, 76 | - Fix installation issues related to missing tornado dependency (#10, 77 | - Make simplejson requirement optional 78 | - 0.2.0 - released *2014-07-22* 79 | - Extract the ``passport`` app to a standalone library 80 | - 0.1.2 - released *2014-05-06* 81 | - consulate cli app bugfixes 82 | - 0.1.0 - released *2014-05-06* 83 | - Initial release 84 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | consulate 2 | ========= 3 | Consulate is a Python client library and set of application for the Consul 4 | service discovery and configuration system. 5 | 6 | |Version| |Downloads| |License| 7 | 8 | Installation 9 | ------------ 10 | consulate may be installed via the Python package index with the tool of 11 | your choice. I prefer pip: 12 | 13 | .. code:: bash 14 | 15 | pip install consulate 16 | 17 | Requirements 18 | ------------ 19 | - requests 20 | 21 | API Documentation 22 | ----------------- 23 | 24 | .. toctree:: 25 | :glob: 26 | :maxdepth: 2 27 | 28 | consul 29 | acl 30 | agent 31 | catalog 32 | events 33 | health 34 | kv 35 | session 36 | status 37 | 38 | 39 | Version History 40 | --------------- 41 | See :doc:`history` 42 | 43 | Issues 44 | ------ 45 | Please report any issues to the Github project at `https://github.com/gmr/consulate/issues `_ 46 | 47 | Source 48 | ------ 49 | consulate source is available on Github at `https://github.com/gmr/consulate `_ 50 | 51 | License 52 | ------- 53 | consulate is released under the `3-Clause BSD license `_. 54 | 55 | Indices and tables 56 | ------------------ 57 | 58 | * :ref:`genindex` 59 | * :ref:`modindex` 60 | * :ref:`search` 61 | 62 | 63 | .. |Version| image:: https://img.shields.io/pypi/v/consulate.svg? 64 | :target: http://badge.fury.io/py/consulate 65 | 66 | .. |Downloads| image:: https://img.shields.io/pypi/dm/consulate.svg? 67 | :target: https://pypi.python.org/pypi/consulate 68 | 69 | .. |License| image:: https://img.shields.io/pypi/l/consulate.svg? 70 | :target: https://consulate.readthedocs.org 71 | -------------------------------------------------------------------------------- /docs/kv.rst: -------------------------------------------------------------------------------- 1 | KV 2 | == 3 | The :py:class:`KV ` class provides both high and low level access 4 | to the Consul Key/Value service. To use the :py:class:`KV ` class, 5 | access the :py:meth:`consulate.Consul.kv` attribute of the 6 | :py:class:`Consul ` class. 7 | 8 | For high-level operation, the :py:class:`KV ` class behaves 9 | like a standard Python :py:class:`dict`. You can get, set, and delete items in 10 | the Key/Value service just as you would with a normal dictionary. 11 | 12 | If you need to have access to the full record associated with an item, there are 13 | lower level methods such as :py:meth:`KV.set_record ` 14 | and :py:meth:`KV.get_record `. These two methods 15 | provide access to the other fields associated with the item in Consul, including 16 | the ``flag`` and various index related fields. 17 | 18 | Examples of Use 19 | --------------- 20 | Here's a big blob of example code that uses most of the functionality in the 21 | :py:class:`KV ` class. Check the comments in the code to see what 22 | part of the class it is demonstrating. 23 | 24 | .. code:: python 25 | 26 | import consulate 27 | 28 | session = consulate.Consul() 29 | 30 | # Set the key named release_flag to True 31 | session.kv['release_flag'] = True 32 | 33 | # Get the value for the release_flag, if not set, raises AttributeError 34 | try: 35 | should_release_feature = session.kv['release_flag'] 36 | except AttributeError: 37 | should_release_feature = False 38 | 39 | # Delete the release_flag key 40 | del session.kv['release_flag'] 41 | 42 | # Fetch how many rows are set in the KV store 43 | print(len(self.session.kv)) 44 | 45 | # Iterate over all keys in the kv store 46 | for key in session.kv: 47 | print('Key "{0}" set'.format(key)) 48 | 49 | # Iterate over all key/value pairs in the kv store 50 | for key, value in session.kv.iteritems(): 51 | print('{0}: {1}'.format(key, value)) 52 | 53 | # Iterate over all keys in the kv store 54 | for value in session.kv.values(): 55 | print(value) 56 | 57 | # Find all keys that start with "fl" 58 | for key in session.kv.find('fl'): 59 | print('Key "{0}" found'.format(key)) 60 | 61 | # Check to see if a key called "foo" is set 62 | if "foo" in session.kv: 63 | print 'Already Set' 64 | 65 | # Return all of the items in the key/value store 66 | session.kv.items() 67 | 68 | API 69 | --- 70 | .. autoclass:: consulate.api.kv.KV 71 | :members: 72 | :special-members: 73 | -------------------------------------------------------------------------------- /docs/lock.rst: -------------------------------------------------------------------------------- 1 | Lock 2 | ==== 3 | 4 | .. autoclass:: consulate.api.lock.Lock 5 | :members: 6 | :special-members: 7 | -------------------------------------------------------------------------------- /docs/session.rst: -------------------------------------------------------------------------------- 1 | Session 2 | ======= 3 | 4 | .. autoclass:: consulate.api.session.Session 5 | :members: 6 | :special-members: 7 | -------------------------------------------------------------------------------- /docs/status.rst: -------------------------------------------------------------------------------- 1 | Status 2 | ====== 3 | 4 | .. autoclass:: consulate.api.status.Status 5 | :members: 6 | :special-members: 7 | -------------------------------------------------------------------------------- /requires/installation.txt: -------------------------------------------------------------------------------- 1 | requests>=2.0.0,<3.0.0 2 | -------------------------------------------------------------------------------- /requires/optional.txt: -------------------------------------------------------------------------------- 1 | requests-unixsocket>=0.1.4,<=1.0.0 2 | -------------------------------------------------------------------------------- /requires/testing.txt: -------------------------------------------------------------------------------- 1 | -r installation.txt 2 | -r optional.txt 3 | coverage 4 | flake8 5 | httmock 6 | mock 7 | nose 8 | sphinx 9 | tornado>=3.0.0,<=5 10 | yapf 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | exclude = .git,build,dist,docs,env 6 | 7 | [nosetests] 8 | cover-branches = 1 9 | cover-erase = 1 10 | cover-html = 1 11 | cover-html-dir = build/coverage 12 | cover-package = consulate 13 | cover-tests = 1 14 | logging-level = DEBUG 15 | stop = 1 16 | verbosity = 2 17 | with-coverage = 1 18 | detailed-errors = 1 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name='abaez.consulate', 5 | version='1.1.0', 6 | description='A Client library and command line application for the Consul', 7 | maintainer='Gavin M. Roy', 8 | maintainer_email='gavinr@aweber.com', 9 | url='https://consulate.readthedocs.org', 10 | install_requires=['requests>=2.0.0,<3.0.0'], 11 | extras_require={'unixsocket': ['requests-unixsocket>=0.1.4,<=1.0.0']}, 12 | license='BSD', 13 | package_data={'': ['LICENSE', 'README.rst']}, 14 | packages=['consulate', 'consulate.api', 'consulate.models'], 15 | entry_points=dict(console_scripts=['consulate=consulate.cli:main']), 16 | classifiers=[ 17 | 'Development Status :: 5 - Production/Stable', 18 | 'Intended Audience :: Developers', 19 | 'License :: OSI Approved :: BSD License', 20 | 'Operating System :: OS Independent', 21 | 'Programming Language :: Python :: 2', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3.3', 25 | 'Programming Language :: Python :: 3.4', 26 | 'Programming Language :: Python :: 3.5', 27 | 'Programming Language :: Python :: 3.6', 28 | 'Programming Language :: Python :: Implementation :: CPython', 29 | 'Programming Language :: Python :: Implementation :: PyPy', 30 | 'Topic :: System :: Systems Administration', 31 | 'Topic :: System :: Clustering', 32 | 'Topic :: Internet :: WWW/HTTP', 33 | 'Topic :: Software Development :: Libraries' 34 | ], 35 | zip_safe=True) 36 | -------------------------------------------------------------------------------- /testing/consul.json: -------------------------------------------------------------------------------- 1 | { 2 | "acl": { 3 | "enabled": true, 4 | "enable_key_list_policy": true, 5 | "tokens": { 6 | "master": "9ae5fe1a-6b38-47e5-a0e7-f06b8b2fa645" 7 | } 8 | }, 9 | "bootstrap_expect": 1, 10 | "data_dir": "/tmp/consul", 11 | "datacenter": "test", 12 | "server": true, 13 | "bind_addr": "{{ GetPrivateIP }}", 14 | "client_addr": "0.0.0.0", 15 | "enable_script_checks": true 16 | } 17 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ['ASYNC_TEST_TIMEOUT'] = os.environ.get('ASYNC_TEST_TIMEOUT', '15') 4 | 5 | 6 | def setup_module(): 7 | with open('build/test-environment') as env_file: 8 | for line in env_file: 9 | if line.startswith('export '): 10 | line = line[7:].strip() 11 | name, _, value = line.partition('=') 12 | if value.startswith(('"', "'")): 13 | if value.endswith(value[0]): 14 | value = value[1:-1] 15 | os.environ[name] = value 16 | -------------------------------------------------------------------------------- /tests/acl_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for Consulate.acl 3 | 4 | """ 5 | import json 6 | import uuid 7 | import random 8 | 9 | import httmock 10 | 11 | import consulate 12 | from consulate import exceptions 13 | 14 | from . import base 15 | 16 | ACL_OLD_RULES = """key "" { 17 | policy = "read" 18 | } 19 | key "foo/" { 20 | policy = "write" 21 | } 22 | """ 23 | 24 | ACL_NEW_RULES = """key_prefix "" { 25 | policy = "read" 26 | } 27 | key "foo/" { 28 | policy = "write" 29 | } 30 | """ 31 | 32 | ACL_NEW_UPDATE_RULES = """key_prefix "" { 33 | policy = "deny" 34 | } 35 | key "foo/" { 36 | policy = "read" 37 | } 38 | """ 39 | 40 | POLICYLINKS_SAMPLE = [ 41 | dict(Name="policylink_sample"), 42 | ] 43 | 44 | POLICYLINKS_UPDATE_SAMPLE = [ 45 | dict(Name="policylink_sample"), 46 | dict(Name="policylink_update_sample") 47 | ] 48 | 49 | SERVICE_IDENTITIES_SAMPLE = [dict(ServiceName="db", Datacenters=list("dc1"))] 50 | 51 | 52 | class TestCase(base.TestCase): 53 | @staticmethod 54 | def uuidv4(): 55 | return str(uuid.uuid4()) 56 | 57 | @staticmethod 58 | def random(): 59 | return str(random.randint(0, 999999)) 60 | 61 | def _generate_policies(self): 62 | sample = self.consul.acl.create_policy(self.random()) 63 | sample_update = self.consul.acl.create_policy(self.random()) 64 | return [dict(ID=sample["ID"]), dict(ID=sample_update["ID"])] 65 | 66 | def _generate_roles(self): 67 | role = self.consul.acl.create_role(self.random()) 68 | return [dict(ID=role["ID"])] 69 | 70 | def test_bootstrap_request_exception(self): 71 | @httmock.all_requests 72 | def response_content(_url_unused, _request): 73 | raise OSError 74 | 75 | with httmock.HTTMock(response_content): 76 | with self.assertRaises(exceptions.RequestError): 77 | self.consul.acl.bootstrap() 78 | 79 | def test_bootstrap_success(self): 80 | expectation = self.uuidv4() 81 | 82 | @httmock.all_requests 83 | def response_content(_url_unused, request): 84 | return httmock.response(200, json.dumps({'ID': expectation}), {}, 85 | None, 0, request) 86 | 87 | with httmock.HTTMock(response_content): 88 | result = self.consul.acl.bootstrap() 89 | 90 | self.assertEqual(result, expectation) 91 | 92 | def test_bootstrap_raises(self): 93 | with self.assertRaises(consulate.Forbidden): 94 | self.consul.acl.bootstrap() 95 | 96 | def test_clone_bad_acl_id(self): 97 | with self.assertRaises(consulate.Forbidden): 98 | self.consul.acl.clone(self.uuidv4()) 99 | 100 | def test_clone_forbidden(self): 101 | with self.assertRaises(consulate.Forbidden): 102 | self.forbidden_consul.acl.clone(self.uuidv4()) 103 | 104 | def test_create_and_destroy(self): 105 | acl_id = self.consul.acl.create(self.uuidv4()) 106 | self.assertTrue(self.consul.acl.destroy(acl_id)) 107 | 108 | def test_create_with_rules(self): 109 | acl_id = self.consul.acl.create(self.uuidv4(), rules=ACL_OLD_RULES) 110 | value = self.consul.acl.info(acl_id) 111 | self.assertEqual(value['Rules'], ACL_OLD_RULES) 112 | 113 | def test_create_and_info(self): 114 | acl_id = self.consul.acl.create(self.uuidv4()) 115 | self.assertIsNotNone(acl_id) 116 | data = self.consul.acl.info(acl_id) 117 | self.assertIsNotNone(data) 118 | self.assertEqual(acl_id, data.get('ID')) 119 | 120 | def test_create_and_list(self): 121 | acl_id = self.consul.acl.create(self.uuidv4()) 122 | data = self.consul.acl.list() 123 | self.assertIn(acl_id, [r.get('ID') for r in data]) 124 | 125 | def test_create_and_clone(self): 126 | acl_id = self.consul.acl.create(self.uuidv4()) 127 | clone_id = self.consul.acl.clone(acl_id) 128 | data = self.consul.acl.list() 129 | self.assertIn(clone_id, [r.get('ID') for r in data]) 130 | 131 | def test_create_and_update(self): 132 | acl_id = str(self.consul.acl.create(self.uuidv4())) 133 | self.consul.acl.update(acl_id, 'Foo') 134 | data = self.consul.acl.list() 135 | self.assertIn('Foo', [r.get('Name') for r in data]) 136 | self.assertIn(acl_id, [r.get('ID') for r in data]) 137 | 138 | def test_create_forbidden(self): 139 | with self.assertRaises(consulate.Forbidden): 140 | self.forbidden_consul.acl.create(self.uuidv4()) 141 | 142 | def test_destroy_forbidden(self): 143 | with self.assertRaises(consulate.Forbidden): 144 | self.forbidden_consul.acl.destroy(self.uuidv4()) 145 | 146 | def test_info_acl_id_not_found(self): 147 | with self.assertRaises(consulate.NotFound): 148 | self.forbidden_consul.acl.info(self.uuidv4()) 149 | 150 | def test_list_request_exception(self): 151 | with httmock.HTTMock(base.raise_oserror): 152 | with self.assertRaises(exceptions.RequestError): 153 | self.consul.acl.list() 154 | 155 | def test_replication(self): 156 | result = self.forbidden_consul.acl.replication() 157 | self.assertFalse(result['Enabled']) 158 | self.assertFalse(result['Running']) 159 | 160 | def test_update_not_found_adds_new_key(self): 161 | acl_id = self.consul.acl.update(self.uuidv4(), 'Foo2') 162 | data = self.consul.acl.list() 163 | self.assertIn('Foo2', [r.get('Name') for r in data]) 164 | self.assertIn(acl_id, [r.get('ID') for r in data]) 165 | 166 | def test_update_with_rules(self): 167 | acl_id = self.consul.acl.update(self.uuidv4(), 168 | name='test', 169 | rules=ACL_OLD_RULES) 170 | value = self.consul.acl.info(acl_id) 171 | self.assertEqual(value['Rules'], ACL_OLD_RULES) 172 | 173 | def test_update_forbidden(self): 174 | with self.assertRaises(consulate.Forbidden): 175 | self.forbidden_consul.acl.update(self.uuidv4(), name='test') 176 | 177 | # NOTE: Everything above here is deprecated post consul-1.4.0 178 | 179 | def test_create_policy(self): 180 | name = self.random() 181 | result = self.consul.acl.create_policy(name=name, rules=ACL_NEW_RULES) 182 | self.assertEqual(result['Rules'], ACL_NEW_RULES) 183 | 184 | def test_create_and_read_policy(self): 185 | name = self.random() 186 | value = self.consul.acl.create_policy(name=name, rules=ACL_NEW_RULES) 187 | result = self.consul.acl.read_policy(value["ID"]) 188 | self.assertEqual(result['Rules'], ACL_NEW_RULES) 189 | 190 | def test_create_and_update_policy(self): 191 | name = self.random() 192 | value = self.consul.acl.create_policy(name=name, rules=ACL_NEW_RULES) 193 | result = self.consul.acl.update_policy(value["ID"], 194 | str(value["Name"]), 195 | rules=ACL_NEW_UPDATE_RULES) 196 | self.assertGreater(result["ModifyIndex"], result["CreateIndex"]) 197 | 198 | def test_create_and_delete_policy(self): 199 | name = self.random() 200 | value = self.consul.acl.create_policy(name=name, rules=ACL_NEW_RULES) 201 | result = self.consul.acl.delete_policy(value["ID"]) 202 | self.assertTrue(result) 203 | 204 | def test_list_policy_exception(self): 205 | with httmock.HTTMock(base.raise_oserror): 206 | with self.assertRaises(exceptions.RequestError): 207 | self.consul.acl.list_policies() 208 | 209 | def test_create_role(self): 210 | name = self.random() 211 | result = self.consul.acl.create_role( 212 | name=name, 213 | policies=self._generate_policies(), 214 | service_identities=SERVICE_IDENTITIES_SAMPLE) 215 | self.assertEqual(result['Name'], name) 216 | 217 | def test_create_and_read_role(self): 218 | name = self.random() 219 | value = self.consul.acl.create_role( 220 | name=name, 221 | policies=self._generate_policies(), 222 | service_identities=SERVICE_IDENTITIES_SAMPLE) 223 | result = self.consul.acl.read_role(value["ID"]) 224 | self.assertEqual(result['ID'], value['ID']) 225 | 226 | def test_create_and_update_role(self): 227 | name = self.random() 228 | value = self.consul.acl.create_role( 229 | name=name, 230 | policies=self._generate_policies(), 231 | service_identities=SERVICE_IDENTITIES_SAMPLE) 232 | result = self.consul.acl.update_role( 233 | value["ID"], 234 | str(value["Name"]), 235 | policies=self._generate_policies()) 236 | self.assertGreater(result["ModifyIndex"], result["CreateIndex"]) 237 | 238 | def test_create_and_delete_role(self): 239 | name = self.random() 240 | value = self.consul.acl.create_role( 241 | name=name, 242 | policies=self._generate_policies(), 243 | service_identities=SERVICE_IDENTITIES_SAMPLE) 244 | result = self.consul.acl.delete_role(value["ID"]) 245 | self.assertTrue(result) 246 | 247 | def test_list_roles_exception(self): 248 | with httmock.HTTMock(base.raise_oserror): 249 | with self.assertRaises(exceptions.RequestError): 250 | self.consul.acl.list_roles() 251 | 252 | def test_create_token(self): 253 | secret_id = self.uuidv4() 254 | accessor_id = self.uuidv4() 255 | result = self.consul.acl.create_token( 256 | accessor_id=accessor_id, 257 | secret_id=secret_id, 258 | roles=self._generate_roles(), 259 | policies=self._generate_policies(), 260 | service_identities=SERVICE_IDENTITIES_SAMPLE) 261 | self.assertEqual(result['AccessorID'], accessor_id) 262 | self.assertEqual(result['SecretID'], secret_id) 263 | 264 | def test_create_and_read_token(self): 265 | secret_id = self.uuidv4() 266 | accessor_id = self.uuidv4() 267 | value = self.consul.acl.create_token( 268 | accessor_id=accessor_id, 269 | secret_id=secret_id, 270 | roles=self._generate_roles(), 271 | policies=self._generate_policies(), 272 | service_identities=SERVICE_IDENTITIES_SAMPLE) 273 | result = self.consul.acl.read_token(value["AccessorID"]) 274 | self.assertEqual(result['AccessorID'], accessor_id) 275 | 276 | def test_create_and_update_token(self): 277 | secret_id = self.uuidv4() 278 | accessor_id = self.uuidv4() 279 | value = self.consul.acl.create_token( 280 | accessor_id=accessor_id, 281 | secret_id=secret_id, 282 | roles=self._generate_roles(), 283 | policies=self._generate_policies(), 284 | service_identities=SERVICE_IDENTITIES_SAMPLE) 285 | result = self.consul.acl.update_token( 286 | str(value["AccessorID"]), policies=self._generate_policies()) 287 | self.assertGreater(result["ModifyIndex"], result["CreateIndex"]) 288 | 289 | def test_create_and_clone_token(self): 290 | secret_id = self.uuidv4() 291 | accessor_id = self.uuidv4() 292 | clone_description = "clone token of " + accessor_id 293 | value = self.consul.acl.create_token( 294 | accessor_id=accessor_id, 295 | secret_id=secret_id, 296 | roles=self._generate_roles(), 297 | policies=self._generate_policies(), 298 | service_identities=SERVICE_IDENTITIES_SAMPLE) 299 | result = self.consul.acl.clone_token(value["AccessorID"], 300 | description=clone_description) 301 | self.assertEqual(result["Description"], clone_description) 302 | 303 | def test_create_and_delete_token(self): 304 | secret_id = self.uuidv4() 305 | accessor_id = self.uuidv4() 306 | value = self.consul.acl.create_token( 307 | accessor_id=accessor_id, 308 | secret_id=secret_id, 309 | roles=self._generate_roles(), 310 | policies=self._generate_policies(), 311 | service_identities=SERVICE_IDENTITIES_SAMPLE) 312 | result = self.consul.acl.delete_token(value["AccessorID"]) 313 | self.assertTrue(result) 314 | -------------------------------------------------------------------------------- /tests/agent_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for Consulate.agent 3 | 4 | """ 5 | import uuid 6 | 7 | import httmock 8 | 9 | import consulate 10 | from consulate import utils 11 | from consulate.models import agent 12 | 13 | from . import base 14 | 15 | 16 | class TestCase(base.TestCase): 17 | def test_checks(self): 18 | result = self.consul.agent.checks() 19 | self.assertDictEqual(result, {}) 20 | 21 | def test_force_leave(self): 22 | self.assertTrue(self.consul.agent.force_leave(str(uuid.uuid4()))) 23 | 24 | def test_force_leave_forbidden(self): 25 | with self.assertRaises(consulate.Forbidden): 26 | self.forbidden_consul.agent.force_leave(str(uuid.uuid4())) 27 | 28 | def test_join(self): 29 | self.assertTrue(self.consul.agent.join('127.0.0.1')) 30 | 31 | def test_join_forbidden(self): 32 | with self.assertRaises(consulate.Forbidden): 33 | self.forbidden_consul.agent.join('255.255.255.255') 34 | 35 | def test_maintenance(self): 36 | self.consul.agent.maintenance(True, 'testing') 37 | self.consul.agent.maintenance(False) 38 | 39 | def test_maintenance_forbidden(self): 40 | with self.assertRaises(consulate.Forbidden): 41 | self.forbidden_consul.agent.maintenance(True) 42 | 43 | def test_members(self): 44 | result = self.consul.agent.members() 45 | self.assertEqual(len(result), 1) 46 | 47 | def test_members_forbidden(self): 48 | with self.assertRaises(consulate.Forbidden): 49 | self.forbidden_consul.agent.members() 50 | 51 | def test_metrics(self): 52 | result = self.consul.agent.metrics() 53 | self.assertIn('Timestamp', result) 54 | self.assertIn('Gauges', result) 55 | 56 | def test_metrics_forbidden(self): 57 | with self.assertRaises(consulate.Forbidden): 58 | self.forbidden_consul.agent.metrics() 59 | 60 | def test_monitor(self): 61 | for offset, line in enumerate(self.consul.agent.monitor()): 62 | self.assertTrue(utils.is_string(line)) 63 | self.consul.agent.metrics() 64 | if offset > 1: 65 | break 66 | 67 | def test_monitor_request_exception(self): 68 | with httmock.HTTMock(base.raise_oserror): 69 | with self.assertRaises(consulate.RequestError): 70 | for _line in self.consul.agent.monitor(): 71 | break 72 | 73 | def test_monitor_forbidden(self): 74 | with self.assertRaises(consulate.Forbidden): 75 | for line in self.forbidden_consul.agent.monitor(): 76 | self.assertIsInstance(line, str) 77 | break 78 | 79 | def test_reload(self): 80 | self.assertIsNone(self.consul.agent.reload()) 81 | 82 | def test_reload_forbidden(self): 83 | with self.assertRaises(consulate.Forbidden): 84 | self.forbidden_consul.agent.reload() 85 | 86 | def test_self(self): 87 | result = self.consul.agent.self() 88 | self.assertIn('Config', result) 89 | self.assertIn('Coord', result) 90 | self.assertIn('Member', result) 91 | 92 | def test_self_forbidden(self): 93 | with self.assertRaises(consulate.Forbidden): 94 | self.forbidden_consul.agent.self() 95 | 96 | def test_service_registration(self): 97 | self.consul.agent.service.register( 98 | 'test-service', address='10.0.0.1', port=5672, tags=['foo', 'bar'], meta={'foo' : 'bar' }) 99 | self.assertIn('test-service', self.consul.agent.services()) 100 | self.consul.agent.service.deregister('test-service') 101 | 102 | def test_service_maintenance(self): 103 | self.consul.agent.service.register( 104 | 'test-service', address='10.0.0.1', port=5672, tags=['foo', 'bar'], meta={'foo' : 'bar' } ) 105 | self.assertIn('test-service', self.consul.agent.services()) 106 | reason = 'Down for Acceptance' 107 | self.consul.agent.service.maintenance('test-service', reason=reason) 108 | node_in_maintenance = self.consul.catalog.nodes()[0]['Node'] 109 | health_check = self.consul.health.node(node_in_maintenance) 110 | self.assertEqual(len(health_check), 2) 111 | self.assertIn(reason, [check['Notes'] for check in health_check]) 112 | self.consul.agent.service.maintenance('test-service', enable=False) 113 | health_check = self.consul.health.node(node_in_maintenance) 114 | self.assertEqual(len(health_check), 1) 115 | self.assertNotEqual(reason, health_check[0]['Notes']) 116 | self.consul.agent.service.deregister('test-service') 117 | 118 | def test_token(self): 119 | self.assertTrue( 120 | self.consul.agent.token('acl_replication_token', 'foo')) 121 | 122 | def test_token_invalid(self): 123 | with self.assertRaises(ValueError): 124 | self.consul.agent.token('acl_replication_tokens', 'foo') 125 | 126 | def test_token_forbidden(self): 127 | with self.assertRaises(consulate.Forbidden): 128 | self.forbidden_consul.agent.token('acl_replication_token', 'foo') 129 | 130 | 131 | class CheckTestCase(base.TestCase): 132 | 133 | def test_register(self): 134 | self.assertTrue(self.consul.agent.check.register( 135 | str(uuid.uuid4()), http='http://localhost', interval='30s')) 136 | 137 | def test_register_args_and_no_interval(self): 138 | with self.assertRaises(ValueError): 139 | self.consul.agent.check.register( 140 | str(uuid.uuid4()), args=['/bin/true']) 141 | 142 | def test_register_args_and_ttl(self): 143 | with self.assertRaises(ValueError): 144 | self.consul.agent.check.register( 145 | str(uuid.uuid4()), args=['/bin/true'], ttl='30s') 146 | 147 | def test_register_http_and_no_interval(self): 148 | with self.assertRaises(ValueError): 149 | self.consul.agent.check.register( 150 | str(uuid.uuid4()), http='http://localhost') 151 | 152 | def test_register_args_and_http(self): 153 | with self.assertRaises(ValueError): 154 | self.consul.agent.check.register( 155 | str(uuid.uuid4()), args=['/bin/true'], http='http://localhost') 156 | 157 | def test_register_forbidden(self): 158 | with self.assertRaises(consulate.Forbidden): 159 | self.forbidden_consul.agent.check.register( 160 | str(uuid.uuid4()), args=['/bin/true'], interval='30s') 161 | 162 | 163 | class TTLCheckTestCase(base.TestCase): 164 | 165 | def setUp(self): 166 | super(TTLCheckTestCase, self).setUp() 167 | name = str(uuid.uuid4()) 168 | self.assertTrue(self.consul.agent.check.register(name, ttl='30s')) 169 | checks = self.consul.agent.checks() 170 | self.check_id = checks[name]['CheckID'] 171 | 172 | def test_pass(self): 173 | self.assertTrue(self.consul.agent.check.ttl_pass(self.check_id)) 174 | 175 | def test_pass_with_note(self): 176 | self.assertTrue( 177 | self.consul.agent.check.ttl_pass(self.check_id, 'PASS')) 178 | 179 | def test_warn(self): 180 | self.assertTrue(self.consul.agent.check.ttl_warn(self.check_id)) 181 | 182 | def test_warn_with_note(self): 183 | self.assertTrue( 184 | self.consul.agent.check.ttl_warn(self.check_id, 'WARN')) 185 | 186 | def test_fail(self): 187 | self.assertTrue(self.consul.agent.check.ttl_fail(self.check_id)) 188 | 189 | def test_fail_with_note(self): 190 | self.assertTrue( 191 | self.consul.agent.check.ttl_fail(self.check_id, 'FAIL')) 192 | 193 | 194 | class ServiceTestCase(base.TestCase): 195 | 196 | def test_register(self): 197 | self.assertTrue( 198 | self.consul.agent.service.register( 199 | str(uuid.uuid4()), 200 | address='127.0.0.1', 201 | port=80, 202 | check=agent.Check( 203 | name='test', args=['/bin/true'], interval='30s'), 204 | tags=[str(uuid.uuid4())])) 205 | 206 | def test_register_grpc(self): 207 | self.assertTrue( 208 | self.consul.agent.service.register( 209 | str(uuid.uuid4()), 210 | address='127.0.0.1', 211 | port=80, 212 | check=agent.Check( 213 | name='test', grpc='https://grpc/status', interval='30s'))) 214 | 215 | def test_register_http(self): 216 | self.assertTrue( 217 | self.consul.agent.service.register( 218 | str(uuid.uuid4()), 219 | address='127.0.0.1', 220 | port=80, 221 | check=agent.Check( 222 | name='test', http='http://localhost', interval='30s'))) 223 | 224 | def test_register_tcp(self): 225 | self.assertTrue( 226 | self.consul.agent.service.register( 227 | str(uuid.uuid4()), 228 | address='127.0.0.1', 229 | port=80, 230 | check=agent.Check( 231 | name='test', tcp='localhost:80', interval='30s'))) 232 | 233 | def test_register_ttl(self): 234 | self.assertTrue( 235 | self.consul.agent.service.register( 236 | str(uuid.uuid4()), 237 | address='127.0.0.1', 238 | port=80, 239 | check=agent.Check(name='test', ttl='30s'))) 240 | 241 | def test_register_multiple_checks(self): 242 | self.assertTrue( 243 | self.consul.agent.service.register( 244 | str(uuid.uuid4()), 245 | address='127.0.0.1', 246 | port=80, 247 | checks=[ 248 | agent.Check( 249 | name='test1', http='http://netloc', 250 | header={'User-Agent': 'unittest.TestCase'}, 251 | interval='30s'), 252 | agent.Check(name='test2', ttl='30s') 253 | ])) 254 | 255 | def test_register_forbidden(self): 256 | with self.assertRaises(consulate.Forbidden): 257 | self.forbidden_consul.agent.service.register( 258 | str(uuid.uuid4()), 259 | address='127.0.0.1', 260 | port=80) 261 | 262 | def test_register_invalid_check(self): 263 | with self.assertRaises(TypeError): 264 | self.consul.agent.service.register( 265 | str(uuid.uuid4()), 266 | address='127.0.0.1', 267 | check=str(uuid.uuid4())) 268 | 269 | def test_register_invalid_checks(self): 270 | with self.assertRaises(ValueError): 271 | self.consul.agent.service.register( 272 | str(uuid.uuid4()), 273 | address='127.0.0.1', 274 | checks=[str(uuid.uuid4())]) 275 | 276 | def test_register_invalid_port(self): 277 | with self.assertRaises(TypeError): 278 | self.consul.agent.service.register( 279 | str(uuid.uuid4()), 280 | address='127.0.0.1', 281 | port='80') 282 | 283 | def test_register_invalid_tags(self): 284 | with self.assertRaises(TypeError): 285 | self.consul.agent.service.register( 286 | str(uuid.uuid4()), 287 | address='127.0.0.1', 288 | tags=str(uuid.uuid4())) 289 | 290 | def test_register_invalid_interval(self): 291 | with self.assertRaises(TypeError): 292 | self.consul.agent.service.register( 293 | str(uuid.uuid4()), 294 | address='127.0.0.1', 295 | port=80, 296 | check=agent.Check( 297 | name='test', http='http://localhost', interval=30)) 298 | 299 | def test_register_invalid_ttl(self): 300 | with self.assertRaises(TypeError): 301 | self.consul.agent.service.register( 302 | str(uuid.uuid4()), 303 | address='127.0.0.1', 304 | port=80, 305 | check=agent.Check(name='test', ttl=30)) 306 | -------------------------------------------------------------------------------- /tests/api_tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | import httmock 3 | import mock 4 | import unittest 5 | try: 6 | from urllib import parse # Python 3 7 | except ImportError: # pragma: no cover 8 | import urlparse as parse # Python 2 9 | import uuid 10 | 11 | import consulate 12 | from consulate import adapters 13 | from consulate.api import base 14 | 15 | with open('testing/consul.json', 'r') as handle: 16 | CONSUL_CONFIG = json.load(handle) 17 | 18 | SCHEME = 'http' 19 | VERSION = 'v1' 20 | 21 | 22 | class ConsulTests(unittest.TestCase): 23 | @mock.patch('consulate.adapters.Request') 24 | @mock.patch('consulate.api.Agent') 25 | @mock.patch('consulate.api.Catalog') 26 | @mock.patch('consulate.api.KV') 27 | @mock.patch('consulate.api.Health') 28 | @mock.patch('consulate.api.Coordinate') 29 | @mock.patch('consulate.api.ACL') 30 | @mock.patch('consulate.api.Event') 31 | @mock.patch('consulate.api.Session') 32 | @mock.patch('consulate.api.Status') 33 | def setUp(self, status, session, event, acl, health, coordinate, kv, catalog, agent, 34 | adapter): 35 | self.host = '127.0.0.1' 36 | self.port = 8500 37 | self.dc = CONSUL_CONFIG['datacenter'] 38 | self.token = CONSUL_CONFIG['acl']['tokens']['master'] 39 | 40 | self.acl = acl 41 | self.adapter = adapter 42 | self.agent = agent 43 | self.catalog = catalog 44 | self.event = event 45 | self.kv = kv 46 | self.health = health 47 | self.coordinate = coordinate 48 | self.session = session 49 | self.status = status 50 | 51 | self.base_uri = '{0}://{1}:{2}/v1'.format(SCHEME, self.host, self.port) 52 | self.consul = consulate.Consul(self.host, self.port, self.dc, 53 | self.token) 54 | 55 | def test_base_uri(self): 56 | self.assertEquals( 57 | self.consul._base_uri(SCHEME, self.host, self.port), self.base_uri) 58 | 59 | def test_unix_socket_base_uri(self): 60 | expectation = 'http+unix://%2Fvar%2Flib%2Fconsul%2Fconsul.sock/v1' 61 | self.assertEquals( 62 | self.consul._base_uri('http+unix', '/var/lib/consul/consul.sock', 63 | None), expectation) 64 | 65 | def test_acl_initialization(self): 66 | self.assertTrue( 67 | self.acl.called_once_with(self.base_uri, self.adapter, self.dc, 68 | self.token)) 69 | 70 | def test_adapter_initialization(self): 71 | self.assertTrue(self.adapter.called_once_with()) 72 | 73 | def test_agent_initialization(self): 74 | self.assertTrue( 75 | self.agent.called_once_with(self.base_uri, self.adapter, self.dc, 76 | self.token)) 77 | 78 | def test_catalog_initialization(self): 79 | self.assertTrue( 80 | self.catalog.called_once_with(self.base_uri, self.adapter, self.dc, 81 | self.token)) 82 | 83 | def test_events_initialization(self): 84 | self.assertTrue( 85 | self.event.called_once_with(self.base_uri, self.adapter, self.dc, 86 | self.token)) 87 | 88 | def test_kv_initialization(self): 89 | self.assertTrue( 90 | self.kv.called_once_with(self.base_uri, self.adapter, self.dc, 91 | self.token)) 92 | 93 | def test_health_initialization(self): 94 | self.assertTrue( 95 | self.health.called_once_with(self.base_uri, self.adapter, self.dc, 96 | self.token)) 97 | 98 | def test_coordinate_initialization(self): 99 | self.assertTrue( 100 | self.coordinate.called_once_with(self.base_uri, self.adapter, self.dc, 101 | self.token)) 102 | 103 | def test_session_initialization(self): 104 | self.assertTrue( 105 | self.session.called_once_with(self.base_uri, self.adapter, self.dc, 106 | self.token)) 107 | 108 | def test_status_initialization(self): 109 | self.assertTrue( 110 | self.status.called_once_with(self.base_uri, self.adapter, self.dc, 111 | self.token)) 112 | 113 | def test_acl_property(self): 114 | self.assertEqual(self.consul.acl, self.consul._acl) 115 | 116 | def test_agent_property(self): 117 | self.assertEqual(self.consul.agent, self.consul._agent) 118 | 119 | def test_catalog_property(self): 120 | self.assertEqual(self.consul.catalog, self.consul._catalog) 121 | 122 | def test_event_property(self): 123 | self.assertEqual(self.consul.event, self.consul._event) 124 | 125 | def test_health_property(self): 126 | self.assertEqual(self.consul.health, self.consul._health) 127 | 128 | def test_coordinate_property(self): 129 | self.assertEqual(self.consul.coordinate, self.consul._coordinate) 130 | 131 | def test_kv_property(self): 132 | self.assertEqual(self.consul.kv, self.consul._kv) 133 | 134 | def test_status_property(self): 135 | self.assertEqual(self.consul.status, self.consul._status) 136 | 137 | 138 | class EndpointBuildURITests(unittest.TestCase): 139 | def setUp(self): 140 | self.adapter = adapters.Request() 141 | self.base_uri = '{0}://localhost:8500/{1}'.format(SCHEME, VERSION) 142 | self.endpoint = base.Endpoint(self.base_uri, self.adapter) 143 | 144 | def test_adapter_assignment(self): 145 | self.assertEqual(self.endpoint._adapter, self.adapter) 146 | 147 | def test_base_uri_assignment(self): 148 | self.assertEqual(self.endpoint._base_uri, '{0}/endpoint'.format( 149 | self.base_uri)) 150 | 151 | def test_dc_assignment(self): 152 | self.assertIsNone(self.endpoint._dc) 153 | 154 | def test_token_assignment(self): 155 | self.assertIsNone(self.endpoint._token) 156 | 157 | def test_build_uri_with_no_params(self): 158 | result = self.endpoint._build_uri(['foo', 'bar']) 159 | parsed = parse.urlparse(result) 160 | query_params = parse.parse_qs(parsed.query) 161 | self.assertEqual(parsed.scheme, SCHEME) 162 | self.assertEqual(parsed.netloc, 'localhost:8500') 163 | self.assertEqual(parsed.path, '/{0}/endpoint/foo/bar'.format(VERSION)) 164 | self.assertDictEqual(query_params, {}) 165 | 166 | def test_build_uri_with_params(self): 167 | result = self.endpoint._build_uri(['foo', 'bar'], {'baz': 'qux'}) 168 | parsed = parse.urlparse(result) 169 | query_params = parse.parse_qs(parsed.query) 170 | self.assertEqual(parsed.scheme, SCHEME) 171 | self.assertEqual(parsed.netloc, 'localhost:8500') 172 | self.assertEqual(parsed.path, '/{0}/endpoint/foo/bar'.format(VERSION)) 173 | self.assertDictEqual(query_params, {'baz': ['qux']}) 174 | 175 | 176 | class EndpointBuildURIWithDCTests(unittest.TestCase): 177 | def setUp(self): 178 | self.adapter = adapters.Request() 179 | self.base_uri = '{0}://localhost:8500/{1}'.format(SCHEME, VERSION) 180 | self.dc = str(uuid.uuid4()) 181 | self.endpoint = base.Endpoint(self.base_uri, self.adapter, self.dc) 182 | 183 | def test_dc_assignment(self): 184 | self.assertEqual(self.endpoint._dc, self.dc) 185 | 186 | def test_token_assignment(self): 187 | self.assertIsNone(self.endpoint._token) 188 | 189 | def test_build_uri_with_no_params(self): 190 | result = self.endpoint._build_uri(['foo', 'bar']) 191 | parsed = parse.urlparse(result) 192 | query_params = parse.parse_qs(parsed.query) 193 | self.assertEqual(parsed.scheme, SCHEME) 194 | self.assertEqual(parsed.netloc, 'localhost:8500') 195 | self.assertEqual(parsed.path, '/{0}/endpoint/foo/bar'.format(VERSION)) 196 | self.assertDictEqual(query_params, {'dc': [self.dc]}) 197 | 198 | def test_build_uri_with_params(self): 199 | result = self.endpoint._build_uri(['foo', 'bar'], {'baz': 'qux'}) 200 | parsed = parse.urlparse(result) 201 | query_params = parse.parse_qs(parsed.query) 202 | self.assertEqual(parsed.scheme, SCHEME) 203 | self.assertEqual(parsed.netloc, 'localhost:8500') 204 | self.assertEqual(parsed.path, '/{0}/endpoint/foo/bar'.format(VERSION)) 205 | self.assertDictEqual(query_params, {'dc': [self.dc], 'baz': ['qux']}) 206 | 207 | 208 | class EndpointBuildURIWithTokenTests(unittest.TestCase): 209 | def setUp(self): 210 | self.adapter = adapters.Request() 211 | self.base_uri = '{0}://localhost:8500/{1}'.format(SCHEME, VERSION) 212 | self.token = str(uuid.uuid4()) 213 | self.endpoint = base.Endpoint( 214 | self.base_uri, self.adapter, token=self.token) 215 | 216 | def test_dc_assignment(self): 217 | self.assertIsNone(self.endpoint._dc) 218 | 219 | def test_token_assignment(self): 220 | self.assertEqual(self.endpoint._token, self.token) 221 | 222 | def test_build_uri_with_no_params(self): 223 | result = self.endpoint._build_uri(['foo', 'bar']) 224 | parsed = parse.urlparse(result) 225 | query_params = parse.parse_qs(parsed.query) 226 | self.assertEqual(parsed.scheme, SCHEME) 227 | self.assertEqual(parsed.netloc, 'localhost:8500') 228 | self.assertEqual(parsed.path, '/{0}/endpoint/foo/bar'.format(VERSION)) 229 | self.assertDictEqual(query_params, {'token': [self.token]}) 230 | 231 | def test_build_uri_with_params(self): 232 | result = self.endpoint._build_uri(['foo', 'bar'], {'baz': 'qux'}) 233 | parsed = parse.urlparse(result) 234 | query_params = parse.parse_qs(parsed.query) 235 | self.assertEqual(parsed.scheme, SCHEME) 236 | self.assertEqual(parsed.netloc, 'localhost:8500') 237 | self.assertEqual(parsed.path, '/{0}/endpoint/foo/bar'.format(VERSION)) 238 | self.assertDictEqual(query_params, { 239 | 'token': [self.token], 240 | 'baz': ['qux'] 241 | }) 242 | 243 | 244 | class EndpointBuildURIWithDCAndTokenTests(unittest.TestCase): 245 | def setUp(self): 246 | self.adapter = adapters.Request() 247 | self.base_uri = '{0}://localhost:8500/{1}'.format(SCHEME, VERSION) 248 | self.dc = str(uuid.uuid4()) 249 | self.token = str(uuid.uuid4()) 250 | self.endpoint = base.Endpoint(self.base_uri, self.adapter, self.dc, 251 | self.token) 252 | 253 | def test_dc_assignment(self): 254 | self.assertEqual(self.endpoint._dc, self.dc) 255 | 256 | def test_token_assignment(self): 257 | self.assertEqual(self.endpoint._token, self.token) 258 | 259 | def test_build_uri_with_no_params(self): 260 | result = self.endpoint._build_uri(['foo', 'bar']) 261 | parsed = parse.urlparse(result) 262 | query_params = parse.parse_qs(parsed.query) 263 | self.assertEqual(parsed.scheme, SCHEME) 264 | self.assertEqual(parsed.netloc, 'localhost:8500') 265 | self.assertEqual(parsed.path, '/{0}/endpoint/foo/bar'.format(VERSION)) 266 | self.assertDictEqual(query_params, { 267 | 'dc': [self.dc], 268 | 'token': [self.token] 269 | }) 270 | 271 | def test_build_uri_with_params(self): 272 | result = self.endpoint._build_uri(['foo', 'bar'], {'baz': 'qux'}) 273 | parsed = parse.urlparse(result) 274 | query_params = parse.parse_qs(parsed.query) 275 | self.assertEqual(parsed.scheme, SCHEME) 276 | self.assertEqual(parsed.netloc, 'localhost:8500') 277 | self.assertEqual(parsed.path, '/{0}/endpoint/foo/bar'.format(VERSION)) 278 | self.assertDictEqual(query_params, { 279 | 'dc': [self.dc], 280 | 'token': [self.token], 281 | 'baz': ['qux'] 282 | }) 283 | 284 | 285 | class EndpointGetTests(unittest.TestCase): 286 | def setUp(self): 287 | self.adapter = adapters.Request() 288 | self.base_uri = '{0}://localhost:8500/{1}'.format(SCHEME, VERSION) 289 | self.dc = str(uuid.uuid4()) 290 | self.token = str(uuid.uuid4()) 291 | self.endpoint = base.Endpoint(self.base_uri, self.adapter, self.dc, 292 | self.token) 293 | 294 | def test_get_200_returns_response_body(self): 295 | @httmock.all_requests 296 | def response_content(_url_unused, request): 297 | headers = { 298 | 'X-Consul-Index': 4, 299 | 'X-Consul-Knownleader': 'true', 300 | 'X-Consul-Lastcontact': 0, 301 | 'Date': 'Fri, 19 Dec 2014 20:44:28 GMT', 302 | 'Content-Length': 13, 303 | 'Content-Type': 'application/json' 304 | } 305 | content = b'{"consul": []}' 306 | return httmock.response(200, content, headers, None, 0, request) 307 | 308 | with httmock.HTTMock(response_content): 309 | values = self.endpoint._get([str(uuid.uuid4())]) 310 | self.assertEqual(values, {'consul': []}) 311 | 312 | def test_get_404_returns_empty_list(self): 313 | @httmock.all_requests 314 | def response_content(_url_unused, request): 315 | headers = { 316 | 'content-length': 0, 317 | 'content-type': 'text/plain; charset=utf-8' 318 | } 319 | return httmock.response(404, None, headers, None, 0, request) 320 | 321 | with httmock.HTTMock(response_content): 322 | values = self.endpoint._get([str(uuid.uuid4())]) 323 | self.assertEqual(values, []) 324 | 325 | 326 | class EndpointGetListTests(unittest.TestCase): 327 | def setUp(self): 328 | self.adapter = adapters.Request() 329 | self.base_uri = '{0}://localhost:8500/{1}'.format(SCHEME, VERSION) 330 | self.dc = str(uuid.uuid4()) 331 | self.token = str(uuid.uuid4()) 332 | self.endpoint = base.Endpoint(self.base_uri, self.adapter, self.dc, 333 | self.token) 334 | 335 | def test_get_list_200_returns_response_body(self): 336 | @httmock.all_requests 337 | def response_content(_url_unused, request): 338 | headers = { 339 | 'X-Consul-Index': 4, 340 | 'X-Consul-Knownleader': 'true', 341 | 'X-Consul-Lastcontact': 0, 342 | 'Date': 'Fri, 19 Dec 2014 20:44:28 GMT', 343 | 'Content-Length': 13, 344 | 'Content-Type': 'application/json' 345 | } 346 | content = b'{"consul": []}' 347 | return httmock.response(200, content, headers, None, 0, request) 348 | 349 | with httmock.HTTMock(response_content): 350 | values = self.endpoint._get_list([str(uuid.uuid4())]) 351 | self.assertEqual(values, [{'consul': []}]) 352 | 353 | def test_get_list_404_returns_empty_list(self): 354 | @httmock.all_requests 355 | def response_content(_url_unused, request): 356 | headers = { 357 | 'content-length': 0, 358 | 'content-type': 'text/plain; charset=utf-8' 359 | } 360 | return httmock.response(404, None, headers, None, 0, request) 361 | 362 | with httmock.HTTMock(response_content): 363 | values = self.endpoint._get_list([str(uuid.uuid4())]) 364 | self.assertEqual(values, []) 365 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | import os 4 | import unittest 5 | import uuid 6 | 7 | import httmock 8 | 9 | import consulate 10 | from consulate import exceptions 11 | 12 | with open('testing/consul.json', 'r') as handle: 13 | CONSUL_CONFIG = json.load(handle) 14 | 15 | 16 | def generate_key(func): 17 | @functools.wraps(func) 18 | def _decorator(self, *args, **kwargs): 19 | key = str(uuid.uuid4())[0:8] 20 | self.used_keys.append(key) 21 | func(self, key) 22 | 23 | return _decorator 24 | 25 | 26 | @httmock.all_requests 27 | def raise_oserror(_url_unused, _request): 28 | raise OSError 29 | 30 | 31 | class TestCase(unittest.TestCase): 32 | def setUp(self): 33 | self.consul = consulate.Consul( 34 | host=os.environ['CONSUL_HOST'], 35 | port=os.environ['CONSUL_PORT'], 36 | token=CONSUL_CONFIG['acl']['tokens']['master']) 37 | self.forbidden_consul = consulate.Consul( 38 | host=os.environ['CONSUL_HOST'], 39 | port=os.environ['CONSUL_PORT'], 40 | token=str(uuid.uuid4())) 41 | self.used_keys = list() 42 | 43 | def tearDown(self): 44 | for key in self.consul.kv.keys(): 45 | self.consul.kv.delete(key) 46 | 47 | checks = self.consul.agent.checks() 48 | for name in checks: 49 | self.consul.agent.check.deregister(checks[name]['CheckID']) 50 | 51 | services = self.consul.agent.services() 52 | for name in services: 53 | self.consul.agent.service.deregister(services[name]['ID']) 54 | 55 | for acl in self.consul.acl.list_tokens(): 56 | if acl['AccessorID'] == CONSUL_CONFIG['acl']['tokens']['master']: 57 | continue 58 | try: 59 | uuid.UUID(acl['AccessorID']) 60 | self.consul.acl.delete_token(acl['AccessorID']) 61 | except (ValueError, exceptions.ConsulateException): 62 | pass 63 | -------------------------------------------------------------------------------- /tests/base_model_tests.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Tests for the Base Model""" 3 | import unittest 4 | import uuid 5 | 6 | from consulate.models import base 7 | 8 | 9 | class TestModel(base.Model): 10 | """Model to perform tests against""" 11 | __slots__ = ['id', 'serial', 'name', 'value'] 12 | __attributes__ = { 13 | 'id': { 14 | 'key': 'ID', 15 | 'type': uuid.UUID, 16 | 'cast_from': str, 17 | 'cast_to': str, 18 | }, 19 | 'serial': { 20 | 'key': 'Serial', 21 | 'type': int, 22 | 'default': 0, 23 | 'required': True, 24 | 'validator': lambda v, _m: v >= 0, 25 | }, 26 | 'name': { 27 | 'key': 'Name', 28 | 'type': str, 29 | 'required': True 30 | }, 31 | 'value': { 32 | 'type': str 33 | }, 34 | 'type': { 35 | 'key': 'Type', 36 | 'type': str, 37 | 'enum': {'client', 'server'} 38 | } 39 | } 40 | 41 | 42 | class TestCase(unittest.TestCase): 43 | 44 | def test_happy_case_with_defaults(self): 45 | kwargs = { 46 | 'id': uuid.uuid4(), 47 | 'name': str(uuid.uuid4()) 48 | } 49 | model = TestModel(**kwargs) 50 | for key, value in kwargs.items(): 51 | self.assertEqual(getattr(model, key), value) 52 | self.assertEqual(model.serial, 0) 53 | 54 | def test_happy_case_with_all_values(self): 55 | kwargs = { 56 | 'id': uuid.uuid4(), 57 | 'serial': 1, 58 | 'name': str(uuid.uuid4()), 59 | 'value': str(uuid.uuid4()) 60 | } 61 | model = TestModel(**kwargs) 62 | for key, value in kwargs.items(): 63 | self.assertEqual(getattr(model, key), value) 64 | 65 | def test_cast_from_str(self): 66 | expectation = uuid.uuid4() 67 | kwargs = { 68 | 'id': str(expectation), 69 | 'name': str(uuid.uuid4()) 70 | } 71 | model = TestModel(**kwargs) 72 | self.assertEqual(model.id, expectation) 73 | 74 | def test_validator_failure(self): 75 | kwargs = { 76 | 'id': uuid.uuid4(), 77 | 'name': str(uuid.uuid4()), 78 | 'serial': -1 79 | } 80 | with self.assertRaises(ValueError): 81 | TestModel(**kwargs) 82 | 83 | def test_type_failure(self): 84 | kwargs = { 85 | 'id': True, 86 | 'name': str(uuid.uuid4()) 87 | } 88 | with self.assertRaises(TypeError): 89 | TestModel(**kwargs) 90 | 91 | def test_missing_requirement(self): 92 | with self.assertRaises(ValueError): 93 | TestModel() 94 | 95 | def test_invalid_attribute(self): 96 | kwargs = {'name': str(uuid.uuid4()), 'foo': 'bar'} 97 | with self.assertRaises(AttributeError): 98 | TestModel(**kwargs) 99 | 100 | def test_invalid_attribute_assignment(self): 101 | kwargs = {'name': str(uuid.uuid4())} 102 | model = TestModel(**kwargs) 103 | with self.assertRaises(AttributeError): 104 | model.foo = 'bar' 105 | 106 | def test_invalid_enum_assignment(self): 107 | kwargs = {'name': str(uuid.uuid4()), 'type': 'invalid'} 108 | with self.assertRaises(ValueError): 109 | TestModel(**kwargs) 110 | 111 | def test_cast_to_dict(self): 112 | kwargs = { 113 | 'id': uuid.uuid4(), 114 | 'serial': 1, 115 | 'name': str(uuid.uuid4()), 116 | 'value': str(uuid.uuid4()), 117 | 'type': 'client' 118 | } 119 | expectation = { 120 | 'ID': str(kwargs['id']), 121 | 'Serial': kwargs['serial'], 122 | 'Name': kwargs['name'], 123 | 'value': kwargs['value'], 124 | 'Type': kwargs['type'] 125 | } 126 | model = TestModel(**kwargs) 127 | self.assertDictEqual(dict(model), expectation) 128 | 129 | def test_cast_to_dict_only_requirements(self): 130 | kwargs = { 131 | 'serial': 1, 132 | 'name': str(uuid.uuid4()) 133 | } 134 | expectation = { 135 | 'Serial': kwargs['serial'], 136 | 'Name': kwargs['name'] 137 | } 138 | model = TestModel(**kwargs) 139 | self.assertDictEqual(dict(model), expectation) 140 | -------------------------------------------------------------------------------- /tests/catalog_tests.py: -------------------------------------------------------------------------------- 1 | from . import base 2 | 3 | 4 | class TestCatalog(base.TestCase): 5 | def test_catalog_registration(self): 6 | self.consul.catalog.register('test-service', address='10.0.0.1') 7 | self.assertIn('test-service', 8 | [n['Node'] for n in self.consul.catalog.nodes()]) 9 | self.consul.catalog.deregister('test-service') 10 | self.assertNotIn('test-service', 11 | [n['Node'] for n in self.consul.catalog.nodes()]) 12 | -------------------------------------------------------------------------------- /tests/coordinate_tests.py: -------------------------------------------------------------------------------- 1 | from . import base 2 | 3 | class TestCoordinate(base.TestCase): 4 | def test_coordinate(self): 5 | coordinates = self.consul.coordinate.nodes() 6 | self.assertIsInstance(coordinates, list) 7 | -------------------------------------------------------------------------------- /tests/event_tests.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from . import base 4 | 5 | 6 | class TestEvent(base.TestCase): 7 | def test_fire(self): 8 | event_name = 'test-event-%s' % str(uuid.uuid4())[0:8] 9 | response = self.consul.event.fire(event_name) 10 | events = self.consul.event.list(event_name) 11 | if isinstance(events, dict): 12 | self.assertEqual(event_name, events.get('Name')) 13 | self.assertEqual(response, events.get('ID')) 14 | elif isinstance(events, dict): 15 | self.assertIn(event_name, [e.get('Name') for e in events]) 16 | self.assertIn(response, [e.get('ID') for e in events]) 17 | else: 18 | assert False, 'Unexpected return type' 19 | -------------------------------------------------------------------------------- /tests/kv_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import unittest 4 | try: 5 | from urllib import parse # Python 3 6 | except ImportError: # pragma: no cover 7 | import urlparse as parse # Python 2 8 | import uuid 9 | 10 | import httmock 11 | 12 | from consulate import adapters, api, utils 13 | 14 | from . import base 15 | 16 | SCHEME = 'http' 17 | VERSION = 'v1' 18 | 19 | ALL_DATA = (b'[{"CreateIndex":643,"ModifyIndex":643,"LockIndex":0,"Key":"bar",' 20 | b'"Flags":0,"Value":"YmF6"},{"CreateIndex":669,"ModifyIndex":669,"' 21 | b'LockIndex":0,"Key":"baz","Flags":0,"Value":"cXV4"},{"CreateIndex' 22 | b'":666,"ModifyIndex":666,"LockIndex":0,"Key":"corgie","Flags":128' 23 | b',"Value":"ZG9n"},{"CreateIndex":642,"ModifyIndex":642,"LockIndex' 24 | b'":0,"Key":"foo","Flags":0,"Value":"YmFy"},{"CreateIndex":644,"Mo' 25 | b'difyIndex":644,"LockIndex":0,"Key":"quz","Flags":0,"Value":"dHJ1' 26 | b'ZQ=="}]') 27 | 28 | ALL_ITEMS = [{ 29 | 'CreateIndex': 643, 30 | 'Flags': 0, 31 | 'Key': 'bar', 32 | 'LockIndex': 0, 33 | 'ModifyIndex': 643, 34 | 'Value': 'baz' 35 | }, { 36 | 'CreateIndex': 669, 37 | 'Flags': 0, 38 | 'Key': 'baz', 39 | 'LockIndex': 0, 40 | 'ModifyIndex': 669, 41 | 'Value': 'qux' 42 | }, { 43 | 'CreateIndex': 666, 44 | 'Flags': 128, 45 | 'Key': 'corgie', 46 | 'LockIndex': 0, 47 | 'ModifyIndex': 666, 48 | 'Value': 'dog' 49 | }, { 50 | 'CreateIndex': 642, 51 | 'Flags': 0, 52 | 'Key': 'foo', 53 | 'LockIndex': 0, 54 | 'ModifyIndex': 642, 55 | 'Value': 'bar' 56 | }, { 57 | 'CreateIndex': 644, 58 | 'Flags': 0, 59 | 'Key': 'quz', 60 | 'LockIndex': 0, 61 | 'ModifyIndex': 644, 62 | 'Value': 'true' 63 | }] 64 | 65 | 66 | @httmock.all_requests 67 | def kv_all_records_content(_url_unused, request): 68 | return httmock.response( 69 | 200, ALL_DATA, { 70 | 'X-Consul-Index': 4, 71 | 'X-Consul-Knownleader': 'true', 72 | 'X-Consul-Lastcontact': 0, 73 | 'Date': 'Fri, 19 Dec 2014 20:44:28 GMT', 74 | 'Content-Length': len(ALL_DATA), 75 | 'Content-Type': 'application/json' 76 | }, None, 0, request) 77 | 78 | 79 | class KVTests(unittest.TestCase): 80 | def setUp(self): 81 | self.adapter = adapters.Request() 82 | self.base_uri = '{0}://localhost:8500/{1}'.format(SCHEME, VERSION) 83 | self.dc = str(uuid.uuid4()) 84 | self.token = str(uuid.uuid4()) 85 | self.kv = api.KV(self.base_uri, self.adapter, self.dc, self.token) 86 | 87 | def test_contains_evaluates_true(self): 88 | @httmock.all_requests 89 | def response_content(_url_unused, request): 90 | return httmock.response(200, None, {}, None, 0, request) 91 | 92 | with httmock.HTTMock(response_content): 93 | self.assertIn('foo', self.kv) 94 | 95 | def test_contains_evaluates_false(self): 96 | @httmock.all_requests 97 | def response_content(_url_unused, request): 98 | return httmock.response(404, None, {}, None, 0, request) 99 | 100 | with httmock.HTTMock(response_content): 101 | self.assertNotIn('foo', self.kv) 102 | 103 | def test_get_all_items(self): 104 | with httmock.HTTMock(kv_all_records_content): 105 | for index, row in enumerate(self.kv._get_all_items()): 106 | self.assertDictEqual(row, ALL_ITEMS[index]) 107 | 108 | def test_items(self): 109 | with httmock.HTTMock(kv_all_records_content): 110 | for index, row in enumerate(self.kv.items()): 111 | value = {ALL_ITEMS[index]['Key']: ALL_ITEMS[index]['Value']} 112 | self.assertDictEqual(row, value) 113 | 114 | def test_iter(self): 115 | with httmock.HTTMock(kv_all_records_content): 116 | for index, row in enumerate(self.kv): 117 | self.assertEqual(row, ALL_ITEMS[index]['Key']) 118 | 119 | def test_iteritems(self): 120 | with httmock.HTTMock(kv_all_records_content): 121 | for index, row in enumerate(self.kv.iteritems()): 122 | value = (ALL_ITEMS[index]['Key'], ALL_ITEMS[index]['Value']) 123 | self.assertEqual(row, value) 124 | 125 | def test_keys(self): 126 | expectation = [item['Key'] for item in ALL_ITEMS] 127 | with httmock.HTTMock(kv_all_records_content): 128 | self.assertEqual(self.kv.keys(), expectation) 129 | 130 | def test_len(self): 131 | with httmock.HTTMock(kv_all_records_content): 132 | self.assertEqual(len(self.kv), len(ALL_ITEMS)) 133 | 134 | def test_values(self): 135 | with httmock.HTTMock(kv_all_records_content): 136 | for index, row in enumerate(self.kv.values()): 137 | self.assertEqual(row, ALL_ITEMS[index]['Value']) 138 | 139 | 140 | class TestKVGetWithNoKey(base.TestCase): 141 | @base.generate_key 142 | def test_get_is_none(self, key): 143 | self.assertIsNone(self.consul.kv.get(key)) 144 | 145 | @base.generate_key 146 | def test_get_item_raises_key_error(self, key): 147 | self.assertRaises(KeyError, self.consul.kv.__getitem__, key) 148 | 149 | 150 | class TestKVSet(base.TestCase): 151 | @base.generate_key 152 | def test_set_item_del_item(self, key): 153 | self.consul.kv[key] = 'foo' 154 | del self.consul.kv[key] 155 | self.assertNotIn(key, self.consul.kv) 156 | 157 | @base.generate_key 158 | def test_set_item_get_item_bool_value(self, key): 159 | self.consul.kv[key] = True 160 | self.assertTrue(self.consul.kv[key]) 161 | 162 | @base.generate_key 163 | def test_set_path_with_value(self, key): 164 | path = 'path/{0}/'.format(key) 165 | self.consul.kv.set(path, 'bar') 166 | self.assertEqual('bar', self.consul.kv[path[:-1]]) 167 | 168 | @base.generate_key 169 | def test_set_item_get_item_int_value(self, key): 170 | self.consul.kv[key] = 128 171 | self.assertEqual(self.consul.kv[key], '128') 172 | 173 | @base.generate_key 174 | def test_set_item_get_item_str_value(self, key): 175 | self.consul.kv[key] = b'foo' 176 | self.assertEqual(self.consul.kv[key], 'foo') 177 | 178 | @base.generate_key 179 | def test_set_item_get_item_str_value_raw(self, key): 180 | self.consul.kv[key] = 'foo' 181 | self.assertEqual(self.consul.kv.get(key, raw=True), 'foo') 182 | 183 | @base.generate_key 184 | def test_set_get_bool_value(self, key): 185 | self.consul.kv.set(key, True) 186 | self.assertTrue(self.consul.kv.get(key)) 187 | 188 | @base.generate_key 189 | def test_set_get_item_value(self, key): 190 | self.consul.kv.set(key, 128) 191 | self.assertEqual(self.consul.kv.get(key), '128') 192 | 193 | @base.generate_key 194 | def test_set_item_get_item_str_value(self, key): 195 | self.consul.kv.set(key, 'foo') 196 | self.assertEqual(self.consul.kv.get(key), 'foo') 197 | 198 | @base.generate_key 199 | def test_set_item_get_record(self, key): 200 | self.consul.kv.set_record(key, 12, 'record') 201 | record = self.consul.kv.get_record(key) 202 | self.assertEqual('record', record['Value']) 203 | self.assertEqual(12, record['Flags']) 204 | self.assertIsInstance(record, dict) 205 | 206 | @base.generate_key 207 | def test_get_record_fail(self, key): 208 | self.assertEqual(self.consul.kv.get_record(key), None) 209 | 210 | @base.generate_key 211 | def test_set_record_no_replace_get_item_str_value(self, key): 212 | self.consul.kv.set(key, 'foo') 213 | self.consul.kv.set_record(key, 0, 'foo', False) 214 | self.assertEqual(self.consul.kv.get(key), 'foo') 215 | 216 | @base.generate_key 217 | def test_set_record_same_value_get_item_str_value(self, key): 218 | self.consul.kv.set(key, 'foo') 219 | self.consul.kv.set_record(key, 0, 'foo', True) 220 | self.assertEqual(self.consul.kv.get(key), 'foo') 221 | 222 | @base.generate_key 223 | def test_set_item_get_item_dict_value(self, key): 224 | value = {'foo': 'bar'} 225 | expectation = json.dumps(value) 226 | self.consul.kv.set(key, value) 227 | self.assertEqual(self.consul.kv.get(key), expectation) 228 | 229 | @unittest.skipIf(utils.PYTHON3, 'No unicode strings in Python3') 230 | @base.generate_key 231 | def test_set_item_get_item_unicode_value(self, key): 232 | self.consul.kv.set(key, u'I like to ✈') 233 | self.assertEqual(self.consul.kv.get(key), u'I like to ✈') 234 | 235 | @unittest.skipIf(not utils.PYTHON3, 'No native unicode strings in Python2') 236 | @base.generate_key 237 | def test_set_item_get_item_unicode_value(self, key): 238 | self.consul.kv.set(key, 'I like to ✈') 239 | response = self.consul.kv.get(key) 240 | self.assertEqual(response, 'I like to ✈') 241 | 242 | @base.generate_key 243 | def test_set_item_in_records(self, key): 244 | self.consul.kv.set(key, 'zomg') 245 | expectation = (key, 0, 'zomg') 246 | self.assertIn(expectation, self.consul.kv.records()) 247 | 248 | @base.generate_key 249 | def test_set_binary_value(self, key): 250 | value = uuid.uuid4().bytes 251 | self.consul.kv.set(key, value) 252 | expectation = (key, 0, value) 253 | self.assertIn(expectation, self.consul.kv.records()) 254 | 255 | 256 | class TestKVLocking(base.TestCase): 257 | @base.generate_key 258 | def test_acquire_and_release_lock(self, key): 259 | lock_key = str(uuid.uuid4())[0:8] 260 | session_id = self.consul.session.create( 261 | key, behavior='delete', ttl='60s') 262 | self.assertTrue(self.consul.kv.acquire_lock(lock_key, session_id)) 263 | self.assertTrue(self.consul.kv.release_lock(lock_key, session_id)) 264 | self.consul.session.destroy(session_id) 265 | 266 | @base.generate_key 267 | def test_acquire_and_release_lock(self, key): 268 | lock_key = str(uuid.uuid4())[0:8] 269 | sid = self.consul.session.create(key, behavior='delete', ttl='60s') 270 | sid2 = self.consul.session.create( 271 | key + '2', behavior='delete', ttl='60s') 272 | self.assertTrue(self.consul.kv.acquire_lock(lock_key, sid)) 273 | self.assertFalse(self.consul.kv.acquire_lock(lock_key, sid2)) 274 | self.assertTrue(self.consul.kv.release_lock(lock_key, sid)) 275 | self.consul.session.destroy(sid) 276 | self.consul.session.destroy(sid2) 277 | 278 | @base.generate_key 279 | def test_acquire_and_release_lock_with_value(self, key): 280 | lock_key = str(uuid.uuid4())[0:8] 281 | lock_value = str(uuid.uuid4()) 282 | sid = self.consul.session.create(key, behavior='delete', ttl='60s') 283 | sid2 = self.consul.session.create( 284 | key + '2', behavior='delete', ttl='60s') 285 | self.assertTrue(self.consul.kv.acquire_lock(lock_key, sid, lock_value)) 286 | self.assertEqual(self.consul.kv.get(lock_key), lock_value) 287 | self.assertFalse(self.consul.kv.acquire_lock(lock_key, sid2)) 288 | self.assertTrue(self.consul.kv.release_lock(lock_key, sid)) 289 | self.consul.session.destroy(sid) 290 | self.consul.session.destroy(sid2) 291 | -------------------------------------------------------------------------------- /tests/lock_tests.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from . import base 4 | 5 | 6 | class TestLock(base.TestCase): 7 | def test_lock_as_context_manager(self): 8 | value = str(uuid.uuid4()) 9 | with self.consul.lock.acquire(value=value): 10 | self.assertEqual(self.consul.kv.get(self.consul.lock.key), value) 11 | -------------------------------------------------------------------------------- /tests/session_tests.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from . import base 4 | 5 | 6 | class TestSession(base.TestCase): 7 | def setUp(self): 8 | super(TestSession, self).setUp() 9 | self.sessions = list() 10 | 11 | def tearDown(self): 12 | for session in self.sessions: 13 | self.consul.session.destroy(session) 14 | 15 | def test_session_create(self): 16 | name = str(uuid.uuid4())[0:8] 17 | session_id = self.consul.session.create( 18 | name, behavior='delete', ttl='60s') 19 | self.sessions.append(session_id) 20 | self.assertIsNotNone(session_id) 21 | 22 | def test_session_destroy(self): 23 | name = str(uuid.uuid4())[0:8] 24 | session_id = self.consul.session.create( 25 | name, behavior='delete', ttl='60s') 26 | self.consul.session.destroy(session_id) 27 | self.assertNotIn(session_id, 28 | [s.get('ID') for s in self.consul.session.list()]) 29 | 30 | def test_session_info(self): 31 | name = str(uuid.uuid4())[0:8] 32 | session_id = self.consul.session.create( 33 | name, behavior='delete', ttl='60s') 34 | result = self.consul.session.info(session_id) 35 | self.assertEqual(session_id, result.get('ID')) 36 | self.consul.session.destroy(session_id) 37 | 38 | def test_session_renew(self): 39 | name = str(uuid.uuid4())[0:8] 40 | session_id = self.consul.session.create( 41 | name, behavior='delete', ttl='60s') 42 | self.sessions.append(session_id) 43 | self.assertTrue(self.consul.session.renew(session_id)) 44 | -------------------------------------------------------------------------------- /tests/utils_tests.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import unittest 3 | 4 | from consulate import exceptions, utils 5 | 6 | 7 | class QuoteTestCase(unittest.TestCase): 8 | def urlencode_test(self): 9 | self.assertEqual("%2Ffoo%40bar", utils.quote("/foo@bar", "")) 10 | 11 | 12 | class MaybeEncodeTestCase(unittest.TestCase): 13 | @unittest.skipUnless(utils.PYTHON3, 'Python3 Only') 14 | def str_test(self): 15 | self.assertEqual(utils.maybe_encode('foo'), b'foo') 16 | 17 | @unittest.skipUnless(utils.PYTHON3, 'Python3 Only') 18 | def byte_test(self): 19 | self.assertEqual(utils.maybe_encode(b'bar'), b'bar') 20 | 21 | 22 | class Response(object): 23 | def __init__(self, status_code=200, body=b'content'): 24 | self.status_code = status_code 25 | self.body = body 26 | 27 | 28 | class ResponseOkTestCase(unittest.TestCase): 29 | 30 | def test_200(self): 31 | self.assertTrue(utils.response_ok(Response(200, b'ok'))) 32 | 33 | def test_400(self): 34 | with self.assertRaises(exceptions.ClientError): 35 | utils.response_ok(Response(400, b'Bad request')) 36 | 37 | def test_401(self): 38 | with self.assertRaises(exceptions.ACLDisabled): 39 | utils.response_ok(Response(401, b'What ACL?')) 40 | 41 | def test_403(self): 42 | with self.assertRaises(exceptions.Forbidden): 43 | utils.response_ok(Response(403, b'No')) 44 | 45 | def test_404_not_raising(self): 46 | self.assertFalse(utils.response_ok(Response(404, b'not found'))) 47 | 48 | def test_404_raising(self): 49 | with self.assertRaises(exceptions.NotFound): 50 | utils.response_ok(Response(404, b'Not Found'), True) 51 | 52 | def test_500(self): 53 | with self.assertRaises(exceptions.ServerError): 54 | utils.response_ok(Response(500, b'Opps')) 55 | 56 | 57 | 58 | 59 | class ValidateGoDurationTestCase(unittest.TestCase): 60 | 61 | def test_valid_values(self): 62 | for value in {'5µs', '300ms', '-1.5h', '2h45m', '5m', '30s'}: 63 | print('Testing {}'.format(value)) 64 | self.assertTrue(utils.validate_go_interval(value)) 65 | 66 | def test_invalid_values(self): 67 | for value in {'100', '1 year', '5M', '30S'}: 68 | print('Testing {}'.format(value)) 69 | self.assertFalse(utils.validate_go_interval(value)) 70 | 71 | 72 | class ValidateURLTestCase(unittest.TestCase): 73 | 74 | def test_valid_values(self): 75 | for value in {'https://foo', 'http://localhost/bar'}: 76 | print('Testing {}'.format(value)) 77 | self.assertTrue(utils.validate_url(value)) 78 | 79 | def test_invalid_values(self): 80 | for value in {'localhost', 'a'}: 81 | print('Testing {}'.format(value)) 82 | self.assertFalse(utils.validate_url(value)) 83 | 84 | 85 | --------------------------------------------------------------------------------