mirror of https://github.com/sysown/proxysql
commit
b9ea0d92c7
@ -0,0 +1,4 @@
|
||||
nose==1.3.6
|
||||
docker-compose==1.1.0
|
||||
requests==2.4.3
|
||||
MySQL-python==1.2.5
|
||||
@ -0,0 +1,16 @@
|
||||
proxysql:
|
||||
build: ../base/proxysql
|
||||
links:
|
||||
- backend1hostgroup0
|
||||
ports:
|
||||
- "6032:6032"
|
||||
- "6033:6033"
|
||||
|
||||
backend1hostgroup0:
|
||||
build: ./mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
expose:
|
||||
- "3306"
|
||||
ports:
|
||||
- "13306:3306"
|
||||
@ -0,0 +1,7 @@
|
||||
# We are creating a custom Dockerfile for MySQL as there is no easy way to
|
||||
# move a file from host into the container. In our case, it's schema.sql
|
||||
# There is a proposed improvement to "docker cp" but it's still being
|
||||
# discussed (https://github.com/docker/docker/issues/5846).
|
||||
FROM mysql:latest
|
||||
ADD ./schema.sql /tmp/
|
||||
ADD ./import_schema.sh /tmp/
|
||||
@ -0,0 +1 @@
|
||||
cat /tmp/schema.sql | mysql -h 127.0.0.1 -u root -proot
|
||||
@ -0,0 +1,12 @@
|
||||
DROP DATABASE IF EXISTS test;
|
||||
|
||||
CREATE DATABASE test;
|
||||
|
||||
USE test;
|
||||
|
||||
CREATE TABLE strings(value LONGTEXT);
|
||||
|
||||
INSERT INTO strings(value) VALUES('a');
|
||||
INSERT INTO strings(value) VALUES('ab');
|
||||
INSERT INTO strings(value) VALUES('abc');
|
||||
INSERT INTO strings(value) VALUES('abcd');
|
||||
@ -0,0 +1,18 @@
|
||||
datadir="/tmp"
|
||||
|
||||
admin_variables =
|
||||
{
|
||||
admin_credentials="admin:admin"
|
||||
mysql_ifaces="0.0.0.0:6032"
|
||||
refresh_interval=2000
|
||||
debug=true
|
||||
}
|
||||
|
||||
mysql_users =
|
||||
(
|
||||
{
|
||||
username = "root"
|
||||
password = "root"
|
||||
default_hostgroup = 0
|
||||
}
|
||||
)
|
||||
@ -0,0 +1,46 @@
|
||||
# How are the tests built?
|
||||
|
||||
First off, a few words about how the infrastructure of the tests looks like.
|
||||
|
||||
Tests are written in Python, and the services needed for running a test
|
||||
(a ProxySQL instance and one or more MySQL instances) are specified in a
|
||||
docker-compose.yml file and are started by using Docker's docker-compose.
|
||||
|
||||
Tests are ran using nosetests (https://nose.readthedocs.org/en/latest/),
|
||||
Python's de facto leader in terms of how tests are written and ran. The command
|
||||
to run the tests is, from the root of the repository:
|
||||
|
||||
```python
|
||||
nosetests --nocapture
|
||||
```
|
||||
|
||||
The "--nocapture" flag is present in order to have detailed output on what is
|
||||
going on. Otherwise, the output will be suppressed by nosetests to give you only
|
||||
a high-level report of how many tests passed and failed.
|
||||
|
||||
# Where can I find the tests?
|
||||
|
||||
Tests are grouped in scenarios. A __scenario__ specifies a configuration of
|
||||
ProxySQL and MySQL backends, together with initial data to populate the MySQL
|
||||
instances (a text file containing SQL queries).
|
||||
|
||||
The folder "scenarios" found in the root folder of the repository contains
|
||||
these scenarios. There is a "base" folder with common utilities, and then there
|
||||
is one folder for each scenario. For example, "1backend" is the name for the
|
||||
scenario of 1 ProxySQL proxy, and 1 MySQL backend.
|
||||
|
||||
To create such a scenario, the simplest way to go about, is to copy-paste the
|
||||
"1backend" scenario and modify it. Some of the important things to modify:
|
||||
- docker-compose.yml. This is where the list of services is described, and
|
||||
where you actuall specify how many MySQL backends there are, and which ports
|
||||
they expose and how. Be careful, there is a naming convention
|
||||
- mysql/schema.sql. This is where the MySQL backends get populated
|
||||
|
||||
# How do I write a test?
|
||||
|
||||
It's pretty simple. Once you have a working scenario, you write a class in
|
||||
the top-level "test" folder, which inherits from ProxySQLBaseTest. One such
|
||||
example is one_backend_test.py. The only thing which you should specify is
|
||||
the docker-compose filename, and then start querying both the proxy and the
|
||||
MySQL backends and testing assertions by using the `run_query_proxysql` and
|
||||
`run_query_mysql' class methods.
|
||||
@ -0,0 +1,13 @@
|
||||
import MySQLdb
|
||||
|
||||
from proxysql_base_test import ProxySQLBaseTest
|
||||
|
||||
class OneBackendTest(ProxySQLBaseTest):
|
||||
|
||||
DOCKER_COMPOSE_FILE = "./scenarios/1backend"
|
||||
|
||||
def test_select_strings_returns_correct_result(self):
|
||||
|
||||
rows = self.run_query_proxysql("SELECT * FROM strings", "test")
|
||||
self.assertEqual(set([row[0] for row in rows]),
|
||||
set(['a', 'ab', 'abc', 'abcd']))
|
||||
@ -0,0 +1,270 @@
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from unittest import TestCase
|
||||
|
||||
from docker import Client
|
||||
from docker.utils import kwargs_from_env
|
||||
import MySQLdb
|
||||
|
||||
class ProxySQLBaseTest(TestCase):
|
||||
|
||||
DOCKER_COMPOSE_FILE = None
|
||||
PROXYSQL_ADMIN_PORT = 6032
|
||||
PROXYSQL_ADMIN_USERNAME = "admin"
|
||||
PROXYSQL_ADMIN_PASSWORD = "admin"
|
||||
PROXYSQL_RW_PORT = 6033
|
||||
PROXYSQL_RW_USERNAME = "root"
|
||||
PROXYSQL_RW_PASSWORD = "root"
|
||||
|
||||
@classmethod
|
||||
def _startup_docker_services(cls):
|
||||
"""Start up all the docker services necessary to start this test.
|
||||
|
||||
They are specified in the docker compose file specified in the variable
|
||||
cls.DOCKER_COMPOSE_FILE.
|
||||
"""
|
||||
|
||||
# We have to perform docker-compose build + docker-compose up,
|
||||
# instead of just doing the latter because of a bug which will give a
|
||||
# 500 internal error for the Docker bug. When this is fixed, we should
|
||||
# remove this first extra step.
|
||||
subprocess.call(["docker-compose", "build"], cwd=cls.DOCKER_COMPOSE_FILE)
|
||||
subprocess.call(["docker-compose", "up", "-d"], cwd=cls.DOCKER_COMPOSE_FILE)
|
||||
|
||||
@classmethod
|
||||
def _shutdown_docker_services(cls):
|
||||
"""Shut down all the docker services necessary to start this test.
|
||||
|
||||
They are specified in the docker compose file specified in the variable
|
||||
cls.DOCKER_COMPOSE_FILE.
|
||||
"""
|
||||
|
||||
subprocess.call(["docker-compose", "stop"], cwd=cls.DOCKER_COMPOSE_FILE)
|
||||
subprocess.call(["docker-compose", "rm", "--force"], cwd=cls.DOCKER_COMPOSE_FILE)
|
||||
|
||||
@classmethod
|
||||
def _get_proxysql_container(cls):
|
||||
"""Out of all the started docker containers, select the one which
|
||||
represents the proxy instance.
|
||||
|
||||
Note that this only supports one proxy instance for now. This method
|
||||
relies on interogating the Docker daemon via its REST API.
|
||||
"""
|
||||
|
||||
containers = Client(**kwargs_from_env()).containers()
|
||||
for container in containers:
|
||||
if 'proxysql' in container['Image']:
|
||||
return container
|
||||
|
||||
@classmethod
|
||||
def _get_mysql_containers(cls):
|
||||
"""Out of all the started docker containers, select the ones which
|
||||
represent the MySQL backend instances.
|
||||
|
||||
This method relies on interogating the Docker daemon via its REST API.
|
||||
"""
|
||||
|
||||
result = []
|
||||
containers = Client(**kwargs_from_env()).containers()
|
||||
for container in containers:
|
||||
if 'proxysql' not in container['Image']:
|
||||
result.append(container)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _populate_mysql_containers_with_dump(cls):
|
||||
"""Populates the started MySQL backend containers with the specified
|
||||
SQL dump file.
|
||||
|
||||
The reason for doing this __after__ the containers are started is
|
||||
because we want to keep them as generic as possible.
|
||||
"""
|
||||
|
||||
mysql_containers = cls._get_mysql_containers()
|
||||
# We have already added the SQL dump to the container by using
|
||||
# the ADD mysql command in the Dockerfile for mysql -- check it
|
||||
# out. The standard agreed location is at /tmp/schema.sql.
|
||||
#
|
||||
# Unfortunately we can't do this step at runtime due to limitations
|
||||
# on how transfer between host and container is supposed to work by
|
||||
# design. See the Dockerfile for MySQL for more details.
|
||||
for mysql_container in mysql_containers:
|
||||
container_id = mysql_container['Names'][0][1:]
|
||||
subprocess.call(["docker", "exec", container_id, "bash", "/tmp/import_schema.sh"])
|
||||
|
||||
@classmethod
|
||||
def _extract_hostgroup_from_container_name(cls, container_name):
|
||||
"""MySQL backend containers are named using a naming convention:
|
||||
backendXhostgroupY, where X and Y can be multi-digit numbers.
|
||||
This extracts the value of the hostgroup from the container name.
|
||||
|
||||
I made this choice because I wasn't able to find another easy way to
|
||||
associate arbitrary metadata with a Docker container through the
|
||||
docker compose file.
|
||||
"""
|
||||
|
||||
service_name = container_name.split('_')[1]
|
||||
return int(re.search(r'BACKEND(\d+)HOSTGROUP(\d+)', service_name).group(2))
|
||||
|
||||
@classmethod
|
||||
def _extract_port_number_from_uri(cls, uri):
|
||||
"""Given a Docker container URI (exposed as an environment variable by
|
||||
the host linking mechanism), extract the TCP port number from it."""
|
||||
return int(uri.split(':')[2])
|
||||
|
||||
@classmethod
|
||||
def _get_environment_variables_from_container(cls, container_name):
|
||||
"""Retrieve the environment variables from the given container.
|
||||
|
||||
This is useful because the host linking mechanism will expose
|
||||
connectivity information to the linked hosts by the use of environment
|
||||
variables.
|
||||
"""
|
||||
|
||||
output = Client(**kwargs_from_env()).execute(container_name, 'env')
|
||||
result = {}
|
||||
lines = output.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
continue
|
||||
(k, v) = line.split('=')
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _populate_proxy_configuration_with_backends(cls):
|
||||
"""Populate ProxySQL's admin information with the MySQL backends
|
||||
and their associated hostgroups.
|
||||
|
||||
This is needed because I do not want to hardcode this into the ProxySQL
|
||||
config file of the test scenario, as it leaves more room for quick
|
||||
iteration.
|
||||
|
||||
In order to configure ProxySQL with the correct backends, we are using
|
||||
the MySQL admin interface of ProxySQL, and inserting rows into the
|
||||
`mysql_servers` table, which contains a list of which servers go into
|
||||
which hostgroup.
|
||||
"""
|
||||
proxysql_container = cls._get_proxysql_container()
|
||||
mysql_containers = cls._get_mysql_containers()
|
||||
environment_variables = cls._get_environment_variables_from_container(
|
||||
proxysql_container['Names'][0][1:])
|
||||
|
||||
proxy_admin_connection = MySQLdb.connect("127.0.0.1",
|
||||
cls.PROXYSQL_ADMIN_USERNAME,
|
||||
cls.PROXYSQL_ADMIN_PASSWORD,
|
||||
port=cls.PROXYSQL_ADMIN_PORT)
|
||||
cursor = proxy_admin_connection.cursor()
|
||||
|
||||
for mysql_container in mysql_containers:
|
||||
container_name = mysql_container['Names'][0][1:].upper()
|
||||
port_uri = environment_variables['%s_PORT' % container_name]
|
||||
port_no = cls._extract_port_number_from_uri(port_uri)
|
||||
ip = environment_variables['%s_PORT_%d_TCP_ADDR' % (container_name, port_no)]
|
||||
hostgroup = cls._extract_hostgroup_from_container_name(container_name)
|
||||
cursor.execute("INSERT INTO mysql_servers(hostgroup_id, hostname, port, status) "
|
||||
"VALUES(%d, '%s', %d, 'ONLINE')" %
|
||||
(hostgroup, ip, port_no))
|
||||
|
||||
cursor.execute("LOAD MYSQL SERVERS TO RUNTIME")
|
||||
cursor.close()
|
||||
proxy_admin_connection.close()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Always shutdown docker services because the previous test might have
|
||||
# left them in limbo.
|
||||
cls._shutdown_docker_services()
|
||||
|
||||
cls._startup_docker_services()
|
||||
|
||||
# Sleep for 30 seconds because we want to populate the MySQL containers
|
||||
# with SQL dumps, but there is a race condition because we do not know
|
||||
# when the MySQL daemons inside them have actually started or not.
|
||||
# TODO(andrei): find a better solution
|
||||
time.sleep(30)
|
||||
cls._populate_mysql_containers_with_dump()
|
||||
|
||||
cls._populate_proxy_configuration_with_backends()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls._shutdown_docker_services()
|
||||
|
||||
def run_query_proxysql(self, query, db, return_result=True):
|
||||
"""Run a query against the ProxySQL proxy and optionally return its
|
||||
results as a set of rows."""
|
||||
proxy_connection = MySQLdb.connect("127.0.0.1",
|
||||
ProxySQLBaseTest.PROXYSQL_RW_USERNAME,
|
||||
ProxySQLBaseTest.PROXYSQL_RW_PASSWORD,
|
||||
port=ProxySQLBaseTest.PROXYSQL_RW_PORT,
|
||||
db=db)
|
||||
cursor = proxy_connection.cursor()
|
||||
cursor.execute(query)
|
||||
if return_result:
|
||||
rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
proxy_connection.close()
|
||||
if return_result:
|
||||
return rows
|
||||
|
||||
def run_query_mysql(self, query, db, return_result=True, hostgroup=0):
|
||||
"""Run a query against the MySQL backend and optionally return its
|
||||
results as a set of rows.
|
||||
|
||||
IMPORTANT: since the queries are actually ran against the MySQL backend,
|
||||
that backend needs to expose its MySQL port to the outside through
|
||||
docker compose's port mapping mechanism.
|
||||
|
||||
This will actually parse the docker-compose configuration file to
|
||||
retrieve the available backends and hostgroups and will pick a backend
|
||||
from the specified hostgroup."""
|
||||
|
||||
# Figure out which are the containers for the specified hostgroup
|
||||
mysql_backends = ProxySQLBaseTest._get_mysql_containers()
|
||||
mysql_backends_in_hostgroup = []
|
||||
for backend in mysql_backends:
|
||||
container_name = backend['Names'][0][1:].upper()
|
||||
backend_hostgroup = ProxySQLBaseTest._extract_hostgroup_from_container_name(container_name)
|
||||
|
||||
mysql_port_exposed=False
|
||||
if not backend.get('Ports'):
|
||||
continue
|
||||
for exposed_port in backend.get('Ports', []):
|
||||
if exposed_port['PrivatePort'] == 3306:
|
||||
mysql_port_exposed = True
|
||||
|
||||
if backend_hostgroup == hostgroup and mysql_port_exposed:
|
||||
mysql_backends_in_hostgroup.append(backend)
|
||||
|
||||
if len(mysql_backends_in_hostgroup) == 0:
|
||||
raise Exception('No backends with a publicly exposed port were '
|
||||
'found in hostgroup %d' % hostgroup)
|
||||
|
||||
# Pick a random container, extract its connection details
|
||||
container = random.choice(mysql_backends_in_hostgroup)
|
||||
for exposed_port in container.get('Ports', []):
|
||||
if exposed_port['PrivatePort'] == 3306:
|
||||
mysql_port = exposed_port['PublicPort']
|
||||
|
||||
mysql_connection = MySQLdb.connect("127.0.0.1",
|
||||
# Warning: this assumes that ProxySQL
|
||||
# and all the backends have the same
|
||||
# credentials.
|
||||
# TODO(andrei): revisit this assumption
|
||||
# in authentication tests.
|
||||
ProxySQLBaseTest.PROXYSQL_RW_USERNAME,
|
||||
ProxySQLBaseTest.PROXYSQL_RW_PASSWORD,
|
||||
port=mysql_port,
|
||||
db=db)
|
||||
cursor = mysql_connection.cursor()
|
||||
cursor.execute(query)
|
||||
if return_result:
|
||||
rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
mysql_connection.close()
|
||||
if return_result:
|
||||
return rows
|
||||
Loading…
Reference in new issue