diff --git a/features/environment.py b/features/environment.py index 3fc52a2d..88937b87 100644 --- a/features/environment.py +++ b/features/environment.py @@ -118,6 +118,7 @@ class PatroniController(AbstractController): with open(patroni_config_name) as f: config = yaml.safe_load(f) + config.pop('etcd') host = config['postgresql']['listen'].split(':')[0] @@ -139,16 +140,6 @@ class PatroniController(AbstractController): if tags: config['tags'] = tags - if dcs != 'etcd': - dcs_config = config.pop('etcd') - dcs_config.pop('host') - - if dcs == 'exhibitor': - dcs_config.update({'hosts': ['127.0.0.1'], 'port': 8181}) - elif dcs == 'zookeeper': - dcs_config['hosts'] = ['127.0.0.1:2181'] - config[dcs] = dcs_config - with open(patroni_config_path, 'w') as f: yaml.safe_dump(config, f, default_flow_style=False) @@ -220,6 +211,7 @@ class ConsulController(AbstractDcsController): def __init__(self, output_dir): super(ConsulController, self).__init__('consul', tempfile.mkdtemp(), output_dir) + os.environ['PATRONI_CONSUL_HOST'] = 'localhost:8500' self._client = consul.Consul() def _start(self): @@ -252,6 +244,7 @@ class EtcdController(AbstractDcsController): def __init__(self, output_dir): super(EtcdController, self).__init__('etcd', tempfile.mkdtemp(), output_dir) + os.environ['PATRONI_ETCD_HOST'] = 'localhost:4001' self._client = etcd.Client() def _start(self): @@ -287,8 +280,10 @@ class ZooKeeperController(AbstractDcsController): """ handles all zookeeper related tasks, used for the tests setup and cleanup """ - def __init__(self, output_dir): + def __init__(self, output_dir, export_env=True): super(ZooKeeperController, self).__init__('zookeeper', None, output_dir) + if export_env: + os.environ['PATRONI_ZOOKEEPER_HOSTS'] = 'localhost:2181' self._client = kazoo.client.KazooClient() def _start(self): @@ -321,10 +316,17 @@ class ZooKeeperController(AbstractDcsController): return False +class ExhibitorController(ZooKeeperController): + + def __init__(self, output_dir): + super(ExhibitorController, self).__init__(output_dir, False) + os.environ.update({'PATRONI_EXHIBITOR_HOSTS': 'localhost', 'PATRONI_EXHIBITOR_PORT': '8181'}) + + class PatroniPoolController(object): KNOWN_DCS = {'consul': ConsulController, 'etcd': EtcdController, - 'zookeeper': ZooKeeperController, 'exhibitor': ZooKeeperController} + 'zookeeper': ZooKeeperController, 'exhibitor': ExhibitorController} def __init__(self): self._dcs = None @@ -376,8 +378,8 @@ class PatroniPoolController(object): @property def dcs(self): if self._dcs is None: - self._dcs = os.environ.get('DCS', 'etcd') - assert self._dcs in self.KNOWN_DCS, 'Unsupported dcs: ' + self.dcs + self._dcs = os.environ.pop('DCS', 'etcd') + assert self._dcs in self.KNOWN_DCS, 'Unsupported dcs: ' + self._dcs return self._dcs diff --git a/features/patroni_api.feature b/features/patroni_api.feature index 6bbdbeb1..c74bd291 100644 --- a/features/patroni_api.feature +++ b/features/patroni_api.feature @@ -10,9 +10,12 @@ Scenario: check API requests on a stand-alone server And I receive a response role master When I issue a GET request to http://127.0.0.1:8008/replica Then I receive a response code 503 - When I issue an empty POST request to http://127.0.0.1:8008/reinitialize - Then I receive a response code 503 - And I receive a response text "I am the leader, can not reinitialize" + When I run patronictl.py reinit batman postgres0 --force + Then I receive a response returncode 0 + And I receive a response output "reinitialize failed for member postgres0, status code=503, (I am the leader, can not reinitialize)" + When I run patronictl.py failover batman --master postgres0 --force + Then I receive a response returncode 1 + And I receive a response output "Error: No candidates found to failover to" When I issue a POST request to http://127.0.0.1:8008/failover with {"leader": "postgres0"} Then I receive a response code 500 And I receive a response text failover is not possible: cluster does not have members except leader @@ -23,24 +26,24 @@ Scenario: check API requests on a stand-alone server And I receive a response text "No values given for required parameters leader and candidate" Scenario: check local configuration reload - Given I issue an empty POST request to http://127.0.0.1:8008/reload - Then I receive a response code 200 - And I receive a response text nothing changed - When I add tag new_tag new_value to postgres0 config - And I issue an empty POST request to http://127.0.0.1:8008/reload - Then I receive a response code 202 + Given I issue an empty POST request to http://127.0.0.1:8008/reload + Then I receive a response code 200 + And I receive a response text nothing changed + When I add tag new_tag new_value to postgres0 config + And I issue an empty POST request to http://127.0.0.1:8008/reload + Then I receive a response code 202 Scenario: check dynamic configuration change via DCS - Given I issue a PATCH request to http://127.0.0.1:8008/config with {"ttl": 20, "loop_wait": 1, "postgresql": {"parameters": {"max_connections": 101}}} - Then I receive a response code 200 - And I receive a response loop_wait 1 - And Response on GET http://127.0.0.1:8008/patroni contains pending_restart after 11 seconds - When I issue a GET request to http://127.0.0.1:8008/config - Then I receive a response code 200 - And I receive a response loop_wait 1 - When I issue a GET request to http://127.0.0.1:8008/patroni - Then I receive a response code 200 - And I receive a response tags {'tag': 'new_value'} + Given I issue a PATCH request to http://127.0.0.1:8008/config with {"ttl": 20, "loop_wait": 1, "postgresql": {"parameters": {"max_connections": 101}}} + Then I receive a response code 200 + And I receive a response loop_wait 1 + And Response on GET http://127.0.0.1:8008/patroni contains pending_restart after 11 seconds + When I issue a GET request to http://127.0.0.1:8008/config + Then I receive a response code 200 + And I receive a response loop_wait 1 + When I issue a GET request to http://127.0.0.1:8008/patroni + Then I receive a response code 200 + And I receive a response tags {'tag': 'new_value'} Scenario: check API requests for the primary-replica pair Given I start postgres1 @@ -49,26 +52,28 @@ Scenario: check API requests for the primary-replica pair Then I receive a response code 200 And I receive a response state running And I receive a response role replica - When I issue an empty POST request to http://127.0.0.1:8009/reinitialize - Then I receive a response code 200 - When I issue an empty POST request to http://127.0.0.1:8008/restart - Then I receive a response code 200 - And postgres0 role is the primary after 5 seconds - When I sleep for 10 seconds - Then postgres1 role is the secondary after 15 seconds + When I run patronictl.py reinit batman postgres1 --force + Then I receive a response returncode 0 + And I receive a response output "Succesful reinitialize on member postgres1" + When I run patronictl.py restart batman postgres0 --force + Then I receive a response returncode 0 + And I receive a response output "Succesful restart on member postgres0" + And postgres0 role is the primary after 5 seconds + When I sleep for 10 seconds + Then postgres1 role is the secondary after 15 seconds Scenario: check the failover via the API - Given I issue a POST request to http://127.0.0.1:8008/failover with {"leader": "postgres0", "candidate": "postgres1"} - Then I receive a response code 200 + Given I run patronictl.py failover batman --master postgres0 --candidate postgres1 --force + Then I receive a response returncode 0 And postgres1 is a leader after 5 seconds - And postgres1 role is the primary after 5 seconds - And postgres0 role is the secondary after 10 seconds + And postgres1 role is the primary after 5 seconds + And postgres0 role is the secondary after 10 seconds And replication works from postgres1 to postgres0 after 20 seconds Scenario: check the scheduled failover - Given I issue a scheduled failover at http://127.0.0.1:8009 from postgres1 to postgres0 in 1 seconds - Then I receive a response code 202 + Given I issue a scheduled failover from postgres1 to postgres0 in 1 seconds + Then I receive a response returncode 0 And postgres0 is a leader after 20 seconds - And postgres0 role is the primary after 5 seconds - And postgres1 role is the secondary after 10 seconds + And postgres0 role is the primary after 5 seconds + And postgres1 role is the secondary after 10 seconds And replication works from postgres0 to postgres1 after 25 seconds diff --git a/features/steps/patroni_api.py b/features/steps/patroni_api.py index 90c38564..a01ec116 100644 --- a/features/steps/patroni_api.py +++ b/features/steps/patroni_api.py @@ -2,6 +2,8 @@ import json import parse import pytz import requests +import shlex +import subprocess import time import yaml @@ -84,23 +86,39 @@ def do_request(context, request_method, url, data): _set_response(context, r) +@step('I run {cmd}') +def do_run(context, cmd): + cmd = ['coverage', 'run', '--source=patroni', '-p'] + shlex.split(cmd) + try: + response = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + context.status_code = 0 + except subprocess.CalledProcessError as e: + response = e.output + context.status_code = e.returncode + context.response = response.strip() + + @then('I receive a response {component:w} {data}') def check_response(context, component, data): if component == 'code': assert context.status_code == int(data),\ - "status code {0} != {1}, response: {2}".format(context.status_code, int(data), context.response) + "status code {0} != {1}, response: {2}".format(context.status_code, data, context.response) + elif component == 'returncode': + assert context.status_code == int(data), "return code {0} != {1}".format(context.status_code, data) elif component == 'text': assert context.response == data.strip('"'), "response {0} does not contain {1}".format(context.response, data) + elif component == 'output': + assert data.strip('"') in context.response, "response {0} does not contain {1}".format(context.response, data) else: assert component in context.response, "{0} is not part of the response".format(component) assert str(context.response[component]) == str(data), "{0} does not contain {1}".format(component, data) -@step('I issue a scheduled failover at {at_url:url} from {from_host:w} to {to_host:w} in {in_seconds:d} seconds') -def scheduled_failover(context, at_url, from_host, to_host, in_seconds): +@step('I issue a scheduled failover from {from_host:w} to {to_host:w} in {in_seconds:d} seconds') +def scheduled_failover(context, from_host, to_host, in_seconds): context.execute_steps(u""" - Given I issue a POST request to {0}/failover with {{"leader": "{1}", "candidate": "{2}", "scheduled_at": "{3}"}} - """.format(at_url, from_host, to_host, datetime.now(pytz.utc) + timedelta(seconds=int(in_seconds)))) + Given I run patronictl.py failover batman --master {0} --candidate {1} --scheduled "{2}" --force + """.format(from_host, to_host, datetime.now(pytz.utc) + timedelta(seconds=int(in_seconds)))) @step('I add tag {tag:w} {value:w} to {pg_name:w} config')