├── test ├── __init__.py ├── toxiproxy_api_test.py ├── test_helper.py └── toxiproxy_test.py ├── setup.cfg ├── pytest.ini ├── toxiproxy ├── __init__.py ├── exceptions.py ├── utils.py ├── toxic.py ├── api.py ├── proxy.py └── server.py ├── circle.yml ├── tox.ini ├── .travis.yml ├── bin └── start-toxiproxy.sh ├── .editorconfig ├── .gitignore ├── LICENSE ├── setup.py └── README.md /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = test 3 | addopts = -s 4 | -------------------------------------------------------------------------------- /toxiproxy/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # flake8: noqa 3 | 4 | from .proxy import Proxy 5 | from .server import Toxiproxy 6 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | override: 3 | - ./bin/start-toxiproxy.sh 4 | test: 5 | override: 6 | - python setup.py test 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py36, pypy 3 | 4 | [testenv] 5 | deps= 6 | pyyaml 7 | future 8 | requests 9 | pytest 10 | coveralls 11 | commands = 12 | coverage run --source=toxiproxy setup.py test 13 | coveralls 14 | -------------------------------------------------------------------------------- /toxiproxy/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Custom exceptions to match the exceptions used in the Ruby wrapper 4 | ProxyExists = type("ProxyExists", (Exception,), {}) 5 | NotFound = type("NotFound", (Exception,), {}) 6 | InvalidToxic = type("InvalidToxic", (Exception,), {}) 7 | -------------------------------------------------------------------------------- /toxiproxy/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from contextlib import closing 4 | 5 | 6 | def can_connect_to(host, port): 7 | """ Test a connection to a host/port """ 8 | 9 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: 10 | return bool(sock.connect_ex((host, port)) == 0) 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | 9 | # command to install dependencies 10 | install: 11 | - "python setup.py install" 12 | - pip install python-coveralls 13 | 14 | # command to run tests 15 | script: 16 | - "bin/start-toxiproxy.sh" 17 | - "python setup.py test" 18 | 19 | after_success: 20 | coveralls 21 | -------------------------------------------------------------------------------- /toxiproxy/toxic.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | class Toxic(object): 5 | """ Represents a Proxy object """ 6 | 7 | def __init__(self, **kwargs): 8 | """ Initializing the Toxic object """ 9 | 10 | self.type = kwargs["type"] 11 | self.stream = kwargs["stream"] if "stream" in kwargs else "downstream" 12 | self.name = kwargs["name"] if "name" in kwargs else "%s_%s" % (self.type, self.stream) 13 | self.toxicity = kwargs["toxicity"] if "toxicity" in kwargs else 1.0 14 | self.attributes = kwargs["attributes"] if "attributes" in kwargs else {} 15 | -------------------------------------------------------------------------------- /bin/start-toxiproxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | VERSION='v2.1.2' 4 | TOXIPROXY_LOG_DIR=${CIRCLE_ARTIFACTS:-'/tmp'} 5 | 6 | if [[ "$OSTYPE" == "linux"* ]]; then 7 | DOWNLOAD_TYPE="linux-amd64" 8 | elif [[ "$OSTYPE" == "darwin"* ]]; then 9 | DOWNLOAD_TYPE="darwin-amd64" 10 | fi 11 | 12 | echo "[dowload toxiproxy for $DOWNLOAD_TYPE]" 13 | curl --silent -L https://github.com/Shopify/toxiproxy/releases/download/$VERSION/toxiproxy-server-$DOWNLOAD_TYPE -o ./bin/toxiproxy-server 14 | 15 | echo "[start toxiproxy]" 16 | chmod +x ./bin/toxiproxy-server 17 | nohup bash -c "./bin/toxiproxy-server > ${TOXIPROXY_LOG_DIR}/toxiproxy.log 2>&1 &" 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | # 4 space indentation 16 | [*.py] 17 | indent_style = space 18 | indent_size = 4 19 | 20 | [*.json] 21 | indent_style = space 22 | indent_size = 4 23 | 24 | # Tab indentation (no size specified) 25 | [*.js] 26 | indent_style = space 27 | indent_size = 4 28 | 29 | [*.md] 30 | trim_trailing_whitespace = false 31 | 32 | [Makefile] 33 | indent_style = tab 34 | indent_size = 4 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # OS or Editor folders 10 | .DS_Store 11 | ._* 12 | Thumbs.db 13 | .cache 14 | .project 15 | .settings 16 | .tmproj 17 | *.esproj 18 | nbproject 19 | *.sublime-project 20 | *.sublime-workspace 21 | .vscode 22 | 23 | # Distribution / packaging 24 | .Python 25 | env/ 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | .hypothesis/ 55 | .coveralls.yml 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # Ignoring for now 65 | CHANGELOG 66 | toxiproxy-ruby 67 | bin/toxiproxy-server 68 | nohup.out 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Douglas Soares de Andrade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/toxiproxy_api_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import requests 4 | 5 | from toxiproxy.api import validate_response 6 | from toxiproxy.exceptions import NotFound, ProxyExists 7 | 8 | 9 | class IntoxicatedTest(TestCase): 10 | def setUp(self): 11 | self.base_url = "http://127.0.0.1:8474" 12 | 13 | def test_not_found(self): 14 | """ Test an invalid url """ 15 | 16 | url_to_test = "%s/%s" % (self.base_url, "not_found") 17 | 18 | with self.assertRaises(NotFound) as context: 19 | validate_response(requests.get(url_to_test)) 20 | self.assertTrue("404 page not found\n" in context.exception) 21 | 22 | def test_proxy_exists(self): 23 | """ Test that a proxy already exists """ 24 | 25 | url_to_test = "%s/%s" % (self.base_url, "proxies") 26 | 27 | json = { 28 | "upstream": "localhost:3306", 29 | "name": "test_mysql_service" 30 | } 31 | 32 | # Lets create the first proxy 33 | validate_response(requests.post(url_to_test, json=json)) 34 | 35 | with self.assertRaises(ProxyExists) as context: 36 | # Lets create another one to see it breaks 37 | validate_response(requests.post(url_to_test, json=json)) 38 | self.assertTrue("proxy already exists" in context.exception) 39 | 40 | # Delete the created proxy 41 | requests.delete("%s/%s" % (url_to_test, "test_mysql_service")) 42 | -------------------------------------------------------------------------------- /test/test_helper.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | import socketserver 4 | import time 5 | 6 | from contextlib import contextmanager 7 | from builtins import bytes as compat_bytes 8 | 9 | 10 | class TCPRequestHandler(socketserver.StreamRequestHandler): 11 | """ 12 | The request handler class for our server. 13 | """ 14 | 15 | def handle(self): 16 | data = self.rfile.readline().strip() 17 | if data: 18 | self.wfile.write(bytes(b"omgs\n")) 19 | 20 | 21 | @contextmanager 22 | def tcp_server(): 23 | """ Simple TCPServer to help test Toxiproxy """ 24 | 25 | server = socketserver.TCPServer(("127.0.0.1", 0), RequestHandlerClass=TCPRequestHandler) 26 | port = server.server_address[1] 27 | 28 | thread = threading.Thread(target=server.serve_forever) 29 | 30 | try: 31 | thread.start() 32 | yield port 33 | finally: 34 | server.shutdown() 35 | 36 | 37 | def connect_to_proxy(host, port): 38 | """ Connect to a proxy and returns how long the connection lasted """ 39 | 40 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 41 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 42 | sock.connect((host, int(port))) 43 | 44 | try: 45 | before = time.time() 46 | sock.sendall(compat_bytes(b"omg\n")) 47 | sock.recv(1024) 48 | passed = time.time() - before 49 | finally: 50 | sock.close() 51 | 52 | return passed 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | NAME = "toxiproxy" 4 | VERSION = "0.1.0" 5 | DESCRIPTION = "Python library for Toxiproxy" 6 | LONG_DESCRIPTION = """\ 7 | A Python library for controlling Toxiproxy. Can be used in resiliency testing.""" 8 | 9 | setup( 10 | name=NAME, 11 | version=VERSION, 12 | description=DESCRIPTION, 13 | long_description=LONG_DESCRIPTION, 14 | author="Douglas Soares de Andrade", 15 | author_email="contato@douglasandrade.com", 16 | url="https://github.com/douglas/toxiproxy-python", 17 | packages=["toxiproxy"], 18 | scripts=[], 19 | license="MIT License", 20 | install_requires=[ 21 | "future", 22 | "requests" 23 | ], 24 | test_suite="test", 25 | setup_requires=[ 26 | "pytest-runner", 27 | "pytest" 28 | ], 29 | tests_require=[ 30 | "pytest-sugar", 31 | "pytest", 32 | "profilehooks" 33 | ], 34 | platforms="Any", 35 | classifiers=[ 36 | "Development Status :: 5 - Production/Stable", 37 | "Intended Audience :: Developers", 38 | "License :: OSI Approved :: MIT License", 39 | "Operating System :: OS Independent", 40 | "Programming Language :: Python", 41 | "Programming Language :: Python :: 2", 42 | "Programming Language :: Python :: 2.7", 43 | "Programming Language :: Python :: 3", 44 | "Programming Language :: Python :: 3.4", 45 | "Programming Language :: Python :: 3.5", 46 | "Topic :: Software Development", 47 | "Topic :: Software Development :: Libraries", 48 | "Topic :: Software Development :: Libraries :: Python Modules" 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /toxiproxy/api.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import requests 4 | 5 | from future.utils import raise_with_traceback 6 | from .exceptions import ProxyExists, NotFound, InvalidToxic 7 | 8 | 9 | class APIConsumer(object): 10 | """ Toxiproxy API Consumer """ 11 | 12 | host = "127.0.0.1" 13 | port = 8474 14 | base_url = "http://%s:%s" % (host, port) 15 | 16 | @classmethod 17 | def get(cls, url, params=None, **kwargs): 18 | """ Use the GET method to fetch data from the API """ 19 | 20 | endpoint = cls.base_url + url 21 | return validate_response(requests.get(url=endpoint, params=params, **kwargs)) 22 | 23 | @classmethod 24 | def delete(cls, url, **kwargs): 25 | """ Use the DELETE method to delete data from the API """ 26 | 27 | endpoint = cls.base_url + url 28 | return validate_response(requests.delete(url=endpoint, **kwargs)) 29 | 30 | @classmethod 31 | def post(cls, url, data=None, json=None, **kwargs): 32 | """ Use the POST method to post data to the API """ 33 | 34 | endpoint = cls.base_url + url 35 | return validate_response(requests.post(url=endpoint, data=data, json=json, **kwargs)) 36 | 37 | 38 | def validate_response(response): 39 | """ 40 | Handle the received response to make sure that we 41 | will only process valid requests. 42 | """ 43 | 44 | content = response.content 45 | 46 | if response.status_code == 409: 47 | raise_with_traceback(ProxyExists(content)) 48 | elif response.status_code == 404: 49 | raise_with_traceback(NotFound(content)) 50 | elif response.status_code == 400: 51 | raise_with_traceback(InvalidToxic(content)) 52 | 53 | return response 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Circle CI](https://circleci.com/gh/douglas/toxiproxy-python.svg?style=shield)](https://circleci.com/gh/douglas/toxiproxy-python) [![Build Status](https://travis-ci.org/douglas/toxiproxy-python.svg?branch=master)](https://travis-ci.org/douglas/toxiproxy-python) [![Coverage Status](https://coveralls.io/repos/github/douglas/toxiproxy-python/badge.svg?branch=master)](https://coveralls.io/github/douglas/toxiproxy-python?branch=master) [![Code Health](https://landscape.io/github/douglas/toxiproxy-python/master/landscape.svg?style=flat)](https://landscape.io/github/douglas/toxiproxy-python/master) 2 | 3 | # toxiproxy-python (Work in Progress) 4 | 5 | `toxiproxy-python` `0.x` (latest) is compatible with the Toxiproxy `2.x` series. 6 | 7 | [Toxiproxy](https://github.com/shopify/toxiproxy) is a proxy to simulate network 8 | and system conditions. The Python API aims to make it simple to write tests that 9 | ensure your application behaves appropriately under harsh conditions. Before you 10 | can use the Python library, you need to read the [Usage section of the Toxiproxy 11 | README](https://github.com/shopify/toxiproxy#usage). 12 | 13 | ``` 14 | pip install git+https://github.com/douglas/toxiproxy-python.git 15 | ``` 16 | 17 | Make sure the Toxiproxy server is already running. 18 | 19 | For more information about Toxiproxy and the available toxics, see the [Toxiproxy 20 | documentation](https://github.com/shopify/toxiproxy) 21 | 22 | ## Usage (what we want to achieve when this library is ready) 23 | 24 | The Python client communicates with the Toxiproxy daemon via HTTP. By default it 25 | connects to `http://127.0.0.1:8474`, but you can point to any host: 26 | 27 | ``` 28 | to be ported =( 29 | ``` 30 | 31 | For example, to simulate 1000ms latency on a database server you can use the 32 | `latency` toxic with the `latency` argument (see the Toxiproxy project for a 33 | list of all toxics): 34 | 35 | ``` 36 | to be ported =( 37 | ``` 38 | 39 | You can also take an endpoint down for the duration of a block at the TCP level: 40 | 41 | ``` 42 | to be ported =( 43 | ``` 44 | 45 | If you want to simulate all your Redis instances being down: 46 | 47 | ``` 48 | to be ported =( 49 | ``` 50 | 51 | If you want to simulate that your cache server is slow at incoming network 52 | (upstream), but fast at outgoing (downstream), you can apply a toxic to just the 53 | upstream: 54 | 55 | ``` 56 | to be ported =( 57 | ``` 58 | 59 | By default the toxic is applied to the downstream connection, you can be 60 | explicit and chain them: 61 | 62 | ``` 63 | to be ported =( 64 | ``` 65 | 66 | See the [Toxiproxy README](https://github.com/shopify/toxiproxy#Toxics) for a 67 | list of toxics. 68 | 69 | ## Populate 70 | 71 | To populate Toxiproxy pass the proxy configurations to the `populate` method: 72 | 73 | ``` 74 | to be ported =( 75 | ``` 76 | 77 | This will create the proxies passed, or replace the proxies if they already exist in Toxiproxy. 78 | It's recommended to do this early as early in boot as possible, see the 79 | [Toxiproxy README](https://github.com/shopify/toxiproxy#Usage). If you have many 80 | proxies, we recommend storing the Toxiproxy configs in a configuration file and 81 | deserializing it into `populate`. 82 | -------------------------------------------------------------------------------- /toxiproxy/proxy.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from contextlib import contextmanager 4 | 5 | from .api import APIConsumer 6 | from .toxic import Toxic 7 | 8 | 9 | class Proxy(object): 10 | """ Represents a Proxy object """ 11 | 12 | def __init__(self, **kwargs): 13 | 14 | self.name = kwargs["name"] 15 | self.upstream = kwargs["upstream"] 16 | self.enabled = kwargs["enabled"] 17 | self.listen = kwargs["listen"] 18 | 19 | @contextmanager 20 | def down(self): 21 | """ Takes the proxy down while in the context """ 22 | 23 | try: 24 | self.disable() 25 | yield self 26 | finally: 27 | self.enable() 28 | 29 | def toxics(self): 30 | """ Returns all toxics tied to the proxy """ 31 | 32 | toxics = APIConsumer.get("/proxies/%s/toxics" % self.name).json() 33 | toxics_dict = {} 34 | 35 | for toxic in toxics: 36 | toxic_name = toxic["name"] 37 | toxic.update({'proxy': self.name}) 38 | 39 | # Add the new toxic to the proxy toxics collection 40 | toxics_dict.update({toxic_name: Toxic(**toxic)}) 41 | 42 | return toxics_dict 43 | 44 | def get_toxic(self, toxic_name): 45 | """ Retrive a toxic if it exists """ 46 | 47 | toxics = self.toxics() 48 | if toxic_name in toxics: 49 | return toxics[toxic_name] 50 | else: 51 | return None 52 | 53 | def add_toxic(self, **kwargs): 54 | """ Add a toxic to the proxy """ 55 | 56 | toxic_type = kwargs["type"] 57 | stream = kwargs["stream"] if "stream" in kwargs else "downstream" 58 | name = kwargs["name"] if "name" in kwargs else "%s_%s" % (toxic_type, stream) 59 | toxicity = kwargs["toxicity"] if "toxicity" in kwargs else 1.0 60 | attributes = kwargs["attributes"] if "attributes" in kwargs else {} 61 | 62 | # Lets build a dictionary to send the data to create the Toxic 63 | json = { 64 | "name": name, 65 | "type": toxic_type, 66 | "stream": stream, 67 | "toxicity": toxicity, 68 | "attributes": attributes 69 | } 70 | 71 | APIConsumer.post("/proxies/%s/toxics" % self.name, json=json).json() 72 | 73 | def destroy_toxic(self, toxic_name): 74 | """ Destroy the given toxic """ 75 | 76 | delete_url = "/proxies/%s/toxics/%s" % (self.name, toxic_name) 77 | return bool(APIConsumer.delete(delete_url)) 78 | 79 | def destroy(self): 80 | """ Destroy a Toxiproxy proxy """ 81 | 82 | return bool(APIConsumer.delete("/proxies/%s" % self.name)) 83 | 84 | def disable(self): 85 | """ 86 | Disables a Toxiproxy - this will drop all active connections and 87 | stop the proxy from listening. 88 | """ 89 | 90 | return self.__enable_proxy(False) 91 | 92 | def enable(self): 93 | """ 94 | Enables a Toxiproxy - this will cause the proxy to start listening again. 95 | """ 96 | 97 | return self.__enable_proxy(True) 98 | 99 | def __enable_proxy(self, enabled=False): 100 | """ Enables or Disable a proxy """ 101 | 102 | # Lets build a dictionary to send the data to the Toxiproxy server 103 | json = { 104 | "enabled": enabled, 105 | } 106 | 107 | APIConsumer.post("/proxies/%s" % self.name, json=json).json() 108 | self.enabled = enabled 109 | -------------------------------------------------------------------------------- /toxiproxy/server.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from future.utils import raise_with_traceback, viewitems, listvalues 4 | from .api import APIConsumer 5 | from .proxy import Proxy 6 | from .exceptions import ProxyExists 7 | from .utils import can_connect_to 8 | 9 | 10 | class Toxiproxy(object): 11 | """ Represents a Toxiproxy server """ 12 | 13 | def proxies(self): 14 | """ Returns all the proxies registered in the server """ 15 | 16 | proxies = APIConsumer.get("/proxies").json() 17 | proxies_dict = {} 18 | 19 | for name, values in viewitems(proxies): 20 | # Lets create a Proxy object to hold all its data 21 | proxy = Proxy(**values) 22 | 23 | # Add the new proxy to the toxiproxy proxies collection 24 | proxies_dict.update({name: proxy}) 25 | 26 | return proxies_dict 27 | 28 | def destroy_all(self): 29 | proxies = listvalues(self.proxies()) 30 | for proxy in proxies: 31 | self.destroy(proxy) 32 | 33 | def get_proxy(self, proxy_name): 34 | """ Retrive a proxy if it exists """ 35 | 36 | proxies = self.proxies() 37 | if proxy_name in proxies: 38 | return proxies[proxy_name] 39 | else: 40 | return None 41 | 42 | def running(self): 43 | """ Test if the toxiproxy server is running """ 44 | 45 | return can_connect_to(APIConsumer.host, APIConsumer.port) 46 | 47 | def version(self): 48 | """ Get the toxiproxy server version """ 49 | 50 | if self.running() is True: 51 | return APIConsumer.get("/version").content 52 | else: 53 | return None 54 | 55 | def reset(self): 56 | """ Re-enables all proxies and disables all toxics. """ 57 | 58 | return bool(APIConsumer.post("/reset")) 59 | 60 | def create(self, upstream, name, listen=None, enabled=None): 61 | """ Create a toxiproxy proxy """ 62 | 63 | if name in self.proxies(): 64 | raise_with_traceback(ProxyExists("This proxy already exists.")) 65 | 66 | # Lets build a dictionary to send the data to the Toxiproxy server 67 | json = { 68 | "upstream": upstream, 69 | "name": name 70 | } 71 | 72 | if listen is not None: 73 | json["listen"] = listen 74 | else: 75 | json["listen"] = "127.0.0.1:0" 76 | if enabled is not None: 77 | json["enabled"] = enabled 78 | 79 | proxy_info = APIConsumer.post("/proxies", json=json).json() 80 | proxy_info["api_consumer"] = APIConsumer 81 | 82 | # Lets create a Proxy object to hold all its data 83 | proxy = Proxy(**proxy_info) 84 | 85 | return proxy 86 | 87 | def destroy(self, proxy): 88 | """ Delete a toxiproxy proxy """ 89 | 90 | if isinstance(proxy, Proxy): 91 | return proxy.destroy() 92 | else: 93 | return False 94 | 95 | def populate(self, proxies): 96 | """ Create a list of proxies from an array """ 97 | 98 | populated_proxies = [] 99 | 100 | for proxy in proxies: 101 | existing = self.get_proxy(proxy["name"]) 102 | 103 | if existing is not None and (existing.upstream != proxy["upstream"] or existing.listen != proxy["listen"]): 104 | self.destroy(existing) 105 | existing = None 106 | 107 | if existing is None: 108 | proxy_instance = self.create(**proxy) 109 | populated_proxies.append(proxy_instance) 110 | 111 | return populated_proxies 112 | 113 | 114 | def update_api_consumer(self, host, port): 115 | """ Update the APIConsumer host and port """ 116 | 117 | APIConsumer.host = host 118 | APIConsumer.port = port 119 | APIConsumer.base_url = "http://%s:%s" % (host, port) 120 | -------------------------------------------------------------------------------- /test/toxiproxy_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import pytest 4 | 5 | from past.builtins import basestring 6 | 7 | from toxiproxy.exceptions import ProxyExists, InvalidToxic 8 | from toxiproxy import Toxiproxy 9 | from toxiproxy.utils import can_connect_to 10 | 11 | from .test_helper import tcp_server, connect_to_proxy 12 | 13 | # The toxiproxy server we will use for the tests 14 | toxiproxy = Toxiproxy() 15 | 16 | 17 | def teardown_function(function): 18 | """ Lets cler all the created proxies during the tests """ 19 | 20 | toxiproxy.destroy_all() 21 | 22 | 23 | def test_create_proxy(): 24 | """ Test if we can create proxies """ 25 | 26 | proxy = toxiproxy.create( 27 | upstream="localhost:3306", 28 | name="test_mysql_master", 29 | enabled=False, 30 | listen="127.0.0.1:43215" 31 | ) 32 | 33 | assert proxy.upstream == "localhost:3306" 34 | assert proxy.name == "test_mysql_master" 35 | assert proxy.enabled is False 36 | assert proxy.listen == "127.0.0.1:43215" 37 | 38 | 39 | def test_destroy_proxy(): 40 | """ Test if we can destroy proxies """ 41 | 42 | proxy = toxiproxy.create(upstream="localhost:3306", name="test_mysql_master") 43 | toxiproxy.destroy("test_mysql_master") 44 | assert proxy not in toxiproxy.proxies() 45 | 46 | 47 | def test_destroy_invalid_proxy(): 48 | """ Test if we can destroy an invalid proxy """ 49 | 50 | result = toxiproxy.destroy("invalid_proxy") 51 | assert result is False 52 | 53 | 54 | def test_disable_proxy(): 55 | """ Test if we can disable proxies """ 56 | 57 | proxy = toxiproxy.create(upstream="localhost:3306", name="test_mysql_master") 58 | proxy.disable() 59 | 60 | assert proxy.enabled is False 61 | 62 | 63 | def test_enable_proxy(): 64 | """ Test if we can enable a proxy """ 65 | 66 | proxy = toxiproxy.create(upstream="localhost:3306", name="test_mysql_master") 67 | proxy.disable() 68 | proxy.enable() 69 | 70 | assert proxy.enabled is True 71 | 72 | 73 | def test_find_invalid_proxy(): 74 | """ Test if that we cant fetch an invalid proxy """ 75 | 76 | proxy = toxiproxy.get_proxy("invalid_proxy") 77 | assert proxy is None 78 | 79 | 80 | def test_create_and_find_proxy(): 81 | """ Test if we can create a proxy and retrieve it """ 82 | 83 | toxiproxy.create(upstream="localhost:3306", name="test_mysql_master") 84 | proxy = toxiproxy.get_proxy("test_mysql_master") 85 | 86 | assert proxy.upstream == "localhost:3306" 87 | assert proxy.name == "test_mysql_master" 88 | 89 | 90 | def test_cant_create_proxies_same_name(): 91 | """ Test that we can't create proxies with the same name """ 92 | 93 | toxiproxy.create(upstream="localhost:3306", name="test_mysql_master") 94 | 95 | with pytest.raises(ProxyExists) as excinfo: 96 | toxiproxy.create(upstream="localhost:3306", name="test_mysql_master") 97 | assert excinfo.typename == "ProxyExists" 98 | 99 | 100 | def test_version_of_invalid_toxiproxy(): 101 | """ Test that we cant fetch the version of an invalid toxiproxy server """ 102 | 103 | toxiproxy.update_api_consumer("0.0.0.0", 12345) 104 | assert toxiproxy.version() is None 105 | 106 | # Restoring the defaults 107 | toxiproxy.update_api_consumer("127.0.0.1", 8474) 108 | 109 | 110 | def test_proxy_not_running_with_bad_host(): 111 | toxiproxy.update_api_consumer("0.0.0.0", 12345) 112 | assert toxiproxy.running() is False 113 | 114 | # Restoring the defaults 115 | toxiproxy.update_api_consumer("127.0.0.1", 8474) 116 | 117 | 118 | def test_populate_creates_proxies_array(): 119 | """ Test that we can create proxies from an array of proxies """ 120 | 121 | proxies = [ 122 | { 123 | "name": "test_toxiproxy_populate1", 124 | "upstream": "localhost:3306", 125 | "listen": "localhost:22222" 126 | }, 127 | { 128 | "name": "test_toxiproxy_populate2", 129 | "upstream": "localhost:3306", 130 | "listen": "localhost:22223", 131 | }, 132 | ] 133 | 134 | proxies = toxiproxy.populate(proxies) 135 | 136 | for proxy in proxies: 137 | host, port = proxy.listen.split(":") 138 | assert can_connect_to(host, int(port)) is True 139 | 140 | 141 | def test_running_helper(): 142 | """ Test if the wrapper can connect with a valid toxiproxy server """ 143 | 144 | assert toxiproxy.running() is True 145 | 146 | 147 | def test_version(): 148 | """ Test if the version is an instance of a string type """ 149 | 150 | assert isinstance(toxiproxy.version(), basestring) 151 | 152 | 153 | def test_enable_and_disable_proxy_with_toxic(): 154 | """ Test if we can enable and disable a proxy with toxic """ 155 | 156 | with tcp_server() as port: 157 | proxy = toxiproxy.create(upstream="localhost:%s" % port, name="test_rubby_server") 158 | proxy_host, proxy_port = proxy.listen.split(":") 159 | listen_addr = proxy.listen 160 | 161 | proxy.add_toxic(type="latency", attributes={"latency": 123}) 162 | 163 | proxy.disable() 164 | assert can_connect_to(proxy_host, int(proxy_port)) is False 165 | 166 | proxy.enable() 167 | assert can_connect_to(proxy_host, int(proxy_port)) is True 168 | 169 | latency_toxic = proxy.get_toxic("latency_downstream") 170 | assert latency_toxic.attributes['latency'] == 123 171 | 172 | assert listen_addr == proxy.listen 173 | 174 | 175 | def test_delete_toxic(): 176 | """ Test if we can delete a toxic """ 177 | 178 | with tcp_server() as port: 179 | proxy = toxiproxy.create(upstream="localhost:%s" % port, name="test_rubby_server") 180 | proxy_host, proxy_port = proxy.listen.split(":") 181 | listen_addr = proxy.listen 182 | 183 | proxy.add_toxic(type="latency", attributes={"latency": 123}) 184 | 185 | assert can_connect_to(proxy_host, int(proxy_port)) is True 186 | 187 | latency_toxic = proxy.get_toxic("latency_downstream") 188 | assert latency_toxic.attributes['latency'] == 123 189 | 190 | proxy.destroy_toxic("latency_downstream") 191 | assert proxy.toxics() == {} 192 | 193 | assert listen_addr == proxy.listen 194 | 195 | 196 | def test_reset(): 197 | """ Test the reset Toxiproxy feature """ 198 | 199 | with tcp_server() as port: 200 | proxy = toxiproxy.create(upstream="localhost:%s" % port, name="test_rubby_server") 201 | proxy_host, proxy_port = proxy.listen.split(":") 202 | listen_addr = proxy.listen 203 | 204 | proxy.disable() 205 | assert can_connect_to(proxy_host, int(proxy_port)) is False 206 | 207 | proxy.add_toxic(type="latency", attributes={"latency": 123}) 208 | 209 | toxiproxy.reset() 210 | assert can_connect_to(proxy_host, int(proxy_port)) is True 211 | assert proxy.toxics() == {} 212 | assert listen_addr == proxy.listen 213 | 214 | 215 | def test_populate_creates_proxies_update_listen(): 216 | """ Create proxies and tests if they are available """ 217 | 218 | proxies = [{ 219 | "name": "test_toxiproxy_populate1", 220 | "upstream": "localhost:3306", 221 | "listen": "localhost:22222", 222 | }] 223 | 224 | proxies = toxiproxy.populate(proxies) 225 | 226 | proxies = [{ 227 | "name": "test_toxiproxy_populate1", 228 | "upstream": "localhost:3306", 229 | "listen": "localhost:22223", 230 | }] 231 | 232 | proxies = toxiproxy.populate(proxies) 233 | 234 | for proxy in proxies: 235 | host, port = proxy.listen.split(":") 236 | assert can_connect_to(host, int(port)) is True 237 | 238 | 239 | def test_apply_upstream_toxic(): 240 | """ Test that is possible to create upstream toxics """ 241 | 242 | with tcp_server() as port: 243 | proxy = toxiproxy.create(upstream="localhost:%s" % port, name="test_proxy") 244 | proxy_host, proxy_port = proxy.listen.split(":") 245 | proxy.add_toxic(stream="upstream", type="latency", attributes={"latency": 100}) 246 | 247 | passed = connect_to_proxy(proxy_host, proxy_port) 248 | assert passed, pytest.approx(0.100, 0.01) 249 | 250 | 251 | def test_apply_downstream_toxic(): 252 | """ Test that is possible to create downstream toxics """ 253 | 254 | with tcp_server() as port: 255 | proxy = toxiproxy.create(upstream="localhost:%s" % port, name="test_proxy") 256 | proxy_host, proxy_port = proxy.listen.split(":") 257 | proxy.add_toxic(type="latency", attributes={"latency": 100}) 258 | 259 | passed = connect_to_proxy(proxy_host, proxy_port) 260 | assert passed, pytest.approx(0.100, 0.01) 261 | 262 | 263 | def test_invalid_direction(): 264 | """ Test that is not possible to create toxics with invalid direction """ 265 | 266 | with tcp_server() as port: 267 | proxy = toxiproxy.create(upstream="localhost:%s" % port, name="test_rubby_server") 268 | 269 | with pytest.raises(InvalidToxic) as excinfo: 270 | proxy.add_toxic(type="latency", attributes={"latency": 123}, stream="lolstream") 271 | assert excinfo.typename == "InvalidToxic" 272 | 273 | 274 | def test_multiple_of_same_toxic_type(): 275 | """ Test that is possible to create various toxics with the same type """ 276 | 277 | with tcp_server() as port: 278 | proxy = toxiproxy.create(upstream="localhost:%s" % port, name="test_proxy") 279 | proxy_host, proxy_port = proxy.listen.split(":") 280 | proxy.add_toxic(type="latency", attributes={"latency": 100}) 281 | proxy.add_toxic(type="latency", attributes={"latency": 100}, name="second_latency_downstream") 282 | 283 | passed = connect_to_proxy(proxy_host, proxy_port) 284 | assert passed, pytest.approx(0.200, 0.01) 285 | 286 | 287 | def test_take_endpoint_down(): 288 | """ Test that is possible to take the endpoint down inside a context """ 289 | 290 | with tcp_server() as port: 291 | proxy = toxiproxy.create(upstream="localhost:%s" % port, name="test_rubby_server") 292 | proxy_host, proxy_port = proxy.listen.split(":") 293 | listen_addr = proxy.listen 294 | 295 | with proxy.down(): 296 | assert can_connect_to(proxy_host, int(proxy_port)) is False 297 | 298 | assert can_connect_to(proxy_host, int(proxy_port)) is True 299 | assert listen_addr == proxy.listen 300 | 301 | 302 | def test_apply_prolong_toxics(): 303 | """ Test that is possible to prolong toxics """ 304 | 305 | with tcp_server() as port: 306 | proxy = toxiproxy.create(upstream="localhost:%s" % port, name="test_proxy") 307 | proxy_host, proxy_port = proxy.listen.split(":") 308 | proxy.add_toxic(stream="upstream", type="latency", attributes={"latency": 100}) 309 | proxy.add_toxic(type="latency", attributes={"latency": 100}) 310 | 311 | passed = connect_to_proxy(proxy_host, proxy_port) 312 | assert passed, pytest.approx(0.200, 0.01) 313 | 314 | # def test_raises_when_proxy_doesnt_exist(self): 315 | # pass 316 | 317 | # def test_proxies_all_returns_proxy_collection(self): 318 | # pass 319 | 320 | # def test_down_on_proxy_collection_disables_entire_collection(self): 321 | # pass 322 | 323 | # def test_disable_on_proxy_collection(self): 324 | # pass 325 | 326 | # def test_select_from_toxiproxy_collection(self): 327 | # pass 328 | 329 | # def test_grep_returns_toxiproxy_collection(self): 330 | # pass 331 | 332 | # def test_indexing_allows_regexp(self): 333 | # pass 334 | 335 | # def test_toxic_applies_a_downstream_toxic(self): 336 | # pass 337 | 338 | # def test_toxic_default_name_is_type_and_stream(self): 339 | # pass 340 | 341 | # def test_apply_prolong_toxics(self): 342 | # pass 343 | 344 | # def test_apply_toxics_to_collection(self): 345 | # pass 346 | 347 | # def test_populate_creates_proxies_update_upstream(self): 348 | # pass 349 | 350 | # def test_multiple_of_same_toxic_type_with_same_name(self): 351 | # pass 352 | --------------------------------------------------------------------------------