├── .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 |
--------------------------------------------------------------------------------