Merge branch 'master' of github.com:zalando/patroni into feature/dynamic-configuration

This commit is contained in:
Alexander Kukushkin
2016-06-09 11:04:28 +02:00
12 changed files with 158 additions and 122 deletions
+7 -5
View File
@@ -37,10 +37,16 @@ Consul
------ ------
- **host**: the host:port for the Consul endpoint. - **host**: the host:port for the Consul endpoint.
etcd Etcd
---- ----
- **host**: the host:port for the etcd endpoint. - **host**: the host:port for the etcd endpoint.
Exhibitor
---------
- **hosts**: initial list of Exhibitor (ZooKeeper) nodes in format: ['host1', 'host2', 'etc...' ]. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes.
- **poll\_interval**: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor
- **port**: Exhibitor port.
PostgreSQL PostgreSQL
---------- ----------
- **authentication**: - **authentication**:
@@ -76,7 +82,3 @@ REST API
ZooKeeper ZooKeeper
---------- ----------
- **hosts**: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...']. - **hosts**: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...'].
- **exhibitor**: If you are running a ZooKeeper cluster under the Exhibitor supervisory, this section might interest you:
- **hosts**: initial list of Exhibitor (ZooKeeper) nodes in format: ['host1', 'host2', 'etc...' ]. This list updates automatically whenever the Exhibitor (ZooKeeper) cluster topology changes.
- **poll\_interval**: how often the list of ZooKeeper and Exhibitor nodes should be updated from Exhibitor
- **port**: Exhibitor port.
+3 -6
View File
@@ -143,14 +143,11 @@ class PatroniController(AbstractController):
dcs_config = config.pop('etcd') dcs_config = config.pop('etcd')
dcs_config.pop('host') dcs_config.pop('host')
if dcs == 'consul':
config[dcs] = dcs_config
else:
if dcs == 'exhibitor': if dcs == 'exhibitor':
dcs_config['exhibitor'] = {'hosts': ['127.0.0.1'], 'port': 8181} dcs_config.update({'hosts': ['127.0.0.1'], 'port': 8181})
else: elif dcs == 'zookeeper':
dcs_config['hosts'] = ['127.0.0.1:2181'] dcs_config['hosts'] = ['127.0.0.1:2181']
config['zookeeper'] = dcs_config config[dcs] = dcs_config
with open(patroni_config_path, 'w') as f: with open(patroni_config_path, 'w') as f:
yaml.safe_dump(config, f, default_flow_style=False) yaml.safe_dump(config, f, default_flow_style=False)
+1 -1
View File
@@ -26,7 +26,7 @@ CONFIG_DIR_PATH = click.get_app_dir('patroni')
CONFIG_FILE_PATH = os.path.join(CONFIG_DIR_PATH, 'patronictl.yaml') CONFIG_FILE_PATH = os.path.join(CONFIG_DIR_PATH, 'patronictl.yaml')
LOGLEVEL = 'WARNING' LOGLEVEL = 'WARNING'
DCS_DEFAULTS = {'zookeeper': {'port': 2181, 'template': "zookeeper:\n hosts: ['{host}:{port}']"}, DCS_DEFAULTS = {'zookeeper': {'port': 2181, 'template': "zookeeper:\n hosts: ['{host}:{port}']"},
'exhibitor': {'port': 8181, 'template': "zookeeper:\n exhibitor:\n hosts: [{host}]\n port: {port}"}, 'exhibitor': {'port': 8181, 'template': "exhibitor:\n hosts: [{host}]\n port: {port}"},
'consul': {'port': 8500, 'template': "consul:\n host: '{host}:{port}'"}, 'consul': {'port': 8500, 'template': "consul:\n host: '{host}:{port}'"},
'etcd': {'port': 4001, 'template': "etcd:\n host: '{host}:{port}'"}} 'etcd': {'port': 4001, 'template': "etcd:\n host: '{host}:{port}'"}}
+75
View File
@@ -0,0 +1,75 @@
import logging
import random
import requests
import time
from patroni.dcs.zookeeper import ZooKeeper
from patroni.utils import sleep
from requests.exceptions import RequestException
logger = logging.getLogger(__name__)
class ExhibitorEnsembleProvider(object):
TIMEOUT = 3.1
def __init__(self, hosts, port, uri_path='/exhibitor/v1/cluster/list', poll_interval=300):
self._exhibitor_port = port
self._uri_path = uri_path
self._poll_interval = poll_interval
self._exhibitors = hosts
self._master_exhibitors = hosts
self._zookeeper_hosts = ''
self._next_poll = None
while not self.poll():
logger.info('waiting on exhibitor')
sleep(5)
def poll(self):
if self._next_poll and self._next_poll > time.time():
return False
json = self._query_exhibitors(self._exhibitors)
if not json:
json = self._query_exhibitors(self._master_exhibitors)
if isinstance(json, dict) and 'servers' in json and 'port' in json:
self._next_poll = time.time() + self._poll_interval
zookeeper_hosts = ','.join([h + ':' + str(json['port']) for h in sorted(json['servers'])])
if self._zookeeper_hosts != zookeeper_hosts:
logger.info('ZooKeeper connection string has changed: %s => %s', self._zookeeper_hosts, zookeeper_hosts)
self._zookeeper_hosts = zookeeper_hosts
self._exhibitors = json['servers']
return True
return False
def _query_exhibitors(self, exhibitors):
random.shuffle(exhibitors)
for host in exhibitors:
uri = 'http://{0}:{1}{2}'.format(host, self._exhibitor_port, self._uri_path)
try:
response = requests.get(uri, timeout=self.TIMEOUT)
return response.json()
except RequestException:
pass
return None
@property
def zookeeper_hosts(self):
return self._zookeeper_hosts
class Exhibitor(ZooKeeper):
def __init__(self, config):
interval = config.get('poll_interval', 300)
self._ensemble_provider = ExhibitorEnsembleProvider(config['hosts'], config['port'], poll_interval=interval)
config = config.copy()
config['hosts'] = self._ensemble_provider.zookeeper_hosts
super(Exhibitor, self).__init__(config)
def _load_cluster(self):
if self._ensemble_provider.poll():
self._client.set_hosts(self._ensemble_provider.zookeeper_hosts)
return super(Exhibitor, self)._load_cluster()
+3 -70
View File
@@ -1,14 +1,9 @@
import logging import logging
import random
import requests
import time
from kazoo.client import KazooClient, KazooState from kazoo.client import KazooClient, KazooState
from kazoo.exceptions import NoNodeError, NodeExistsError from kazoo.exceptions import NoNodeError, NodeExistsError
from patroni.dcs import AbstractDCS, ClusterConfig, Cluster, Failover, Leader, Member from patroni.dcs import AbstractDCS, ClusterConfig, Cluster, Failover, Leader, Member
from patroni.exceptions import DCSError from patroni.exceptions import DCSError
from patroni.utils import sleep
from requests.exceptions import RequestException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -17,56 +12,6 @@ class ZooKeeperError(DCSError):
pass pass
class ExhibitorEnsembleProvider(object):
TIMEOUT = 3.1
def __init__(self, hosts, port, uri_path='/exhibitor/v1/cluster/list', poll_interval=300):
self._exhibitor_port = port
self._uri_path = uri_path
self._poll_interval = poll_interval
self._exhibitors = hosts
self._master_exhibitors = hosts
self._zookeeper_hosts = ''
self._next_poll = None
while not self.poll():
logger.info('waiting on exhibitor')
sleep(5)
def poll(self):
if self._next_poll and self._next_poll > time.time():
return False
json = self._query_exhibitors(self._exhibitors)
if not json:
json = self._query_exhibitors(self._master_exhibitors)
if isinstance(json, dict) and 'servers' in json and 'port' in json:
self._next_poll = time.time() + self._poll_interval
zookeeper_hosts = ','.join([h + ':' + str(json['port']) for h in sorted(json['servers'])])
if self._zookeeper_hosts != zookeeper_hosts:
logger.info('ZooKeeper connection string has changed: %s => %s', self._zookeeper_hosts, zookeeper_hosts)
self._zookeeper_hosts = zookeeper_hosts
self._exhibitors = json['servers']
return True
return False
def _query_exhibitors(self, exhibitors):
random.shuffle(exhibitors)
for host in exhibitors:
uri = 'http://{0}:{1}{2}'.format(host, self._exhibitor_port, self._uri_path)
try:
response = requests.get(uri, timeout=self.TIMEOUT)
return response.json()
except RequestException:
pass
return None
@property
def zookeeper_hosts(self):
return self._zookeeper_hosts
class ZooKeeper(AbstractDCS): class ZooKeeper(AbstractDCS):
def __init__(self, config): def __init__(self, config):
@@ -76,16 +21,8 @@ class ZooKeeper(AbstractDCS):
if isinstance(hosts, list): if isinstance(hosts, list):
hosts = ','.join(hosts) hosts = ','.join(hosts)
self.exhibitor = None self._client = KazooClient(hosts, timeout=config['ttl'], connection_retry={'max_delay': 1, 'max_tries': -1},
if 'exhibitor' in config: command_retry={'deadline': config['retry_timeout'], 'max_delay': 1, 'max_tries': -1})
exhibitor = config['exhibitor']
interval = exhibitor.get('poll_interval', 300)
self.exhibitor = ExhibitorEnsembleProvider(exhibitor['hosts'], exhibitor['port'], poll_interval=interval)
hosts = self.exhibitor.zookeeper_hosts
self._client = KazooClient(hosts=hosts, timeout=config['ttl'],
command_retry={'deadline': config['retry_timeout'], 'max_delay': 1, 'max_tries': -1},
connection_retry={'max_delay': 1, 'max_tries': -1})
self._client.add_listener(self.session_listener) self._client.add_listener(self.session_listener)
self._my_member_data = None self._my_member_data = None
@@ -104,8 +41,7 @@ class ZooKeeper(AbstractDCS):
def set_ttl(self, ttl): def set_ttl(self, ttl):
ttl = int(ttl * 1000) ttl = int(ttl * 1000)
# I know, it's weird to access private attributes, but there is # I know, it's weird to access private attributes
# no other way to change session_timeout without losing session
if self._client._session_timeout != ttl: if self._client._session_timeout != ttl:
self._client._session_timeout = ttl self._client._session_timeout = ttl
self._client.restart() self._client.restart()
@@ -180,9 +116,6 @@ class ZooKeeper(AbstractDCS):
self._cluster = Cluster(initialize, config, leader, self._last_leader_operation, members, failover) self._cluster = Cluster(initialize, config, leader, self._last_leader_operation, members, failover)
def _load_cluster(self): def _load_cluster(self):
if self.exhibitor and self.exhibitor.poll():
self._client.set_hosts(self.exhibitor.zookeeper_hosts)
if self._fetch_cluster or self._cluster is None: if self._fetch_cluster or self._cluster is None:
try: try:
self._client.retry(self._inner_load_cluster) self._client.retry(self._inner_load_cluster)
+10 -7
View File
@@ -63,6 +63,7 @@ class Postgresql(object):
self.config = config self.config = config
self.name = config['name'] self.name = config['name']
self.scope = config['scope'] self.scope = config['scope']
self._database = config.get('database', 'postgres')
self._data_dir = config['data_dir'] self._data_dir = config['data_dir']
self._pending_restart = False self._pending_restart = False
self._server_parameters = self.get_server_parameters(config) self._server_parameters = self.get_server_parameters(config)
@@ -79,8 +80,9 @@ class Postgresql(object):
self._pgpass = config.get('pgpass') or os.path.join(os.path.expanduser('~'), 'pgpass') self._pgpass = config.get('pgpass') or os.path.join(os.path.expanduser('~'), 'pgpass')
self.callback = config.get('callbacks') or {} self.callback = config.get('callbacks') or {}
self._postgresql_conf = os.path.join(self._data_dir, 'postgresql.conf') config_base_name = config.get('config_base_name', 'postgresql')
self._postgresql_base_conf_name = 'postgresql.base.conf' self._postgresql_conf = os.path.join(self._data_dir, config_base_name + '.conf')
self._postgresql_base_conf_name = config_base_name + '.base.conf'
self._postgresql_base_conf = os.path.join(self._data_dir, self._postgresql_base_conf_name) self._postgresql_base_conf = os.path.join(self._data_dir, self._postgresql_base_conf_name)
self._recovery_conf = os.path.join(self._data_dir, 'recovery.conf') self._recovery_conf = os.path.join(self._data_dir, 'recovery.conf')
self._configuration_to_save = (self._postgresql_conf, self._postgresql_base_conf, self._configuration_to_save = (self._postgresql_conf, self._postgresql_base_conf,
@@ -130,8 +132,8 @@ class Postgresql(object):
def resolve_connection_addresses(self): def resolve_connection_addresses(self):
self._local_address = self.get_local_address() self._local_address = self.get_local_address()
self.connection_string = 'postgres://{username}:{password}@{connect_address}/postgres'.format( self.connection_string = 'postgres://{username}:{password}@{connect_address}/{database}'.format(
connect_address=self._connect_address or self._local_address, **self._replication) connect_address=self._connect_address or self._local_address, database=self._database, **self._replication)
def reload_config(self, config): def reload_config(self, config):
server_parameters = self.get_server_parameters(config) server_parameters = self.get_server_parameters(config)
@@ -244,7 +246,7 @@ class Postgresql(object):
@property @property
def _connect_kwargs(self): def _connect_kwargs(self):
r = parseurl('postgres://{0}/postgres'.format(self._local_address)) r = parseurl('postgres://{0}/{1}'.format(self._local_address, self._database))
if 'username' in self._superuser: if 'username' in self._superuser:
r['user'] = self._superuser['username'] r['user'] = self._superuser['username']
if 'password' in self._superuser: if 'password' in self._superuser:
@@ -613,8 +615,9 @@ class Postgresql(object):
r = parseurl(leader.conn_url) r = parseurl(leader.conn_url)
r.update(self._superuser) r.update(self._superuser)
r['user'] = r.pop('username') r['user'] = r.pop('username')
r['database'] = self._database
env = self.write_pgpass(r) env = self.write_pgpass(r)
pc = "user={user} host={host} port={port} dbname=postgres sslmode=prefer sslcompression=1".format(**r) pc = "user={user} host={host} port={port} dbname={database} sslmode=prefer sslcompression=1".format(**r)
# first run a checkpoint on a promoted master in order # first run a checkpoint on a promoted master in order
# to make it store the new timeline ([email protected]) # to make it store the new timeline ([email protected])
self.checkpoint(r) self.checkpoint(r)
@@ -660,7 +663,7 @@ class Postgresql(object):
for opt, val in sorted((options or {}).items()): for opt, val in sorted((options or {}).items()):
cmd.extend(['-c', '{0}={1}'.format(opt, val)]) cmd.extend(['-c', '{0}={1}'.format(opt, val)])
# need a database name to connect # need a database name to connect
cmd.append('postgres') cmd.append(self._database)
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT) p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT)
if p: if p:
if command: if command:
+1
View File
@@ -50,6 +50,7 @@ bootstrap:
options: options:
- createrole - createrole
- createdb - createdb
postgresql: postgresql:
listen: 127.0.0.1:5432 listen: 127.0.0.1:5432
connect_address: 127.0.0.1:5432 connect_address: 127.0.0.1:5432
+1 -1
View File
@@ -70,7 +70,7 @@ class TestCtl(unittest.TestCase):
assert parse_dcs('') == {'etcd': {'host': 'localhost:4001'}} assert parse_dcs('') == {'etcd': {'host': 'localhost:4001'}}
assert parse_dcs('localhost:8500') == {'consul': {'host': 'localhost:8500'}} assert parse_dcs('localhost:8500') == {'consul': {'host': 'localhost:8500'}}
assert parse_dcs('zookeeper://localhost') == {'zookeeper': {'hosts': ['localhost:2181']}} assert parse_dcs('zookeeper://localhost') == {'zookeeper': {'hosts': ['localhost:2181']}}
assert parse_dcs('exhibitor://dummy') == {'zookeeper': {'exhibitor': {'hosts': ['dummy'], 'port': 8181}}} assert parse_dcs('exhibitor://dummy') == {'exhibitor': {'hosts': ['dummy'], 'port': 8181}}
assert parse_dcs('consul://localhost') == {'consul': {'host': 'localhost:8500'}} assert parse_dcs('consul://localhost') == {'consul': {'host': 'localhost:8500'}}
self.assertRaises(PatroniCtlException, parse_dcs, 'invalid://test') self.assertRaises(PatroniCtlException, parse_dcs, 'invalid://test')
+31
View File
@@ -0,0 +1,31 @@
import unittest
from mock import Mock, patch
from patroni.dcs.exhibitor import ExhibitorEnsembleProvider, Exhibitor
from patroni.dcs.zookeeper import ZooKeeperError
from test_etcd import SleepException, requests_get
from test_zookeeper import MockKazooClient
@patch('requests.get', requests_get)
@patch('time.sleep', Mock(side_effect=SleepException))
class TestExhibitorEnsembleProvider(unittest.TestCase):
def test_init(self):
self.assertRaises(SleepException, ExhibitorEnsembleProvider, ['localhost'], 8181)
def test_poll(self):
self.assertFalse(ExhibitorEnsembleProvider(['exhibitor'], 8181).poll())
class TestExhibitor(unittest.TestCase):
@patch('requests.get', requests_get)
@patch('patroni.dcs.zookeeper.KazooClient', MockKazooClient)
def setUp(self):
self.e = Exhibitor({'hosts': ['localhost', 'exhibitor'], 'port': 8181, 'scope': 'test',
'name': 'foo', 'ttl': 30, 'retry_timeout': 10})
@patch.object(ExhibitorEnsembleProvider, 'poll', Mock(return_value=True))
def test_get_cluster(self):
self.assertRaises(ZooKeeperError, self.e.get_cluster)
+9 -15
View File
@@ -5,8 +5,7 @@ from kazoo.client import KazooState
from kazoo.exceptions import NoNodeError, NodeExistsError from kazoo.exceptions import NoNodeError, NodeExistsError
from kazoo.protocol.states import ZnodeStat from kazoo.protocol.states import ZnodeStat
from mock import Mock, patch from mock import Mock, patch
from patroni.dcs.zookeeper import Leader, ExhibitorEnsembleProvider, ZooKeeper, ZooKeeperError from patroni.dcs.zookeeper import Leader, ZooKeeper, ZooKeeperError
from test_etcd import SleepException, requests_get
class MockKazooClient(Mock): class MockKazooClient(Mock):
@@ -14,6 +13,9 @@ class MockKazooClient(Mock):
leader = False leader = False
exists = True exists = True
def __init__(self, *args, **kwargs):
super(MockKazooClient, self).__init__()
@property @property
def client_id(self): def client_id(self):
return (-1, '') return (-1, '')
@@ -90,21 +92,12 @@ class MockKazooClient(Mock):
raise NoNodeError raise NoNodeError
@patch('requests.get', requests_get)
@patch('time.sleep', Mock(side_effect=SleepException))
class TestExhibitorEnsembleProvider(unittest.TestCase):
def test_init(self):
self.assertRaises(SleepException, ExhibitorEnsembleProvider, ['localhost'], 8181)
class TestZooKeeper(unittest.TestCase): class TestZooKeeper(unittest.TestCase):
@patch('requests.get', requests_get)
@patch('patroni.dcs.zookeeper.KazooClient', MockKazooClient) @patch('patroni.dcs.zookeeper.KazooClient', MockKazooClient)
def setUp(self): def setUp(self):
self.zk = ZooKeeper({'exhibitor': {'hosts': ['localhost', 'exhibitor'], 'port': 8181}, self.zk = ZooKeeper({'hosts': ['localhost:2181'], 'scope': 'test',
'scope': 'test', 'name': 'foo', 'ttl': 30, 'retry_timeout': 10}) 'name': 'foo', 'ttl': 30, 'retry_timeout': 10})
def test_session_listener(self): def test_session_listener(self):
self.zk.session_listener(KazooState.SUSPENDED) self.zk.session_listener(KazooState.SUSPENDED)
@@ -129,11 +122,12 @@ class TestZooKeeper(unittest.TestCase):
def test_get_cluster(self): def test_get_cluster(self):
self.assertRaises(ZooKeeperError, self.zk.get_cluster) self.assertRaises(ZooKeeperError, self.zk.get_cluster)
self.zk.exhibitor.poll = lambda: True
cluster = self.zk.get_cluster() cluster = self.zk.get_cluster()
self.assertIsInstance(cluster.leader, Leader) self.assertIsInstance(cluster.leader, Leader)
self.zk.touch_member('foo') self.zk.touch_member('foo')
self.zk.delete_leader()
def test_delete_leader(self):
self.assertTrue(self.zk.delete_leader())
def test_set_failover_value(self): def test_set_failover_value(self):
self.zk.set_failover_value('') self.zk.set_failover_value('')