mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-26 15:40:21 +00:00
Add patronictl scaffold command.
Creates the cluster structure in DCS as long as the initialize key does not exist. The structure consists of the leader and member keys. Both are intentionally set to never expire, in order to support running the cluster with the master that doesn't run the Patroni (external master). Changes to the DCS code as well, in order to support non-expiring leader and member keys. Some silly default settings had to be applied if Patroni is unable to find the configuration file. In particular, the connect address will point to the localhost. Perhaps we should avoid running with wihtout the valid configuration altogether, but currently there is a valid use-case for this behavior, namely the replicas that are running with the inaccessible master and getting up-to-date with WAL segments only.
This commit is contained in:
+54
-3
@@ -20,8 +20,9 @@ import yaml
|
||||
from click import ClickException
|
||||
from patroni.config import Config
|
||||
from patroni.dcs import get_dcs as _get_dcs
|
||||
from patroni.exceptions import PatroniException
|
||||
from patroni.postgresql import get_conn_kwargs
|
||||
from patroni.exceptions import PatroniException, DCSError
|
||||
from patroni.postgresql import Postgresql, get_conn_kwargs
|
||||
from patroni.api import RestApiServer
|
||||
from prettytable import PrettyTable
|
||||
from six.moves.urllib_parse import urlparse
|
||||
|
||||
@@ -76,7 +77,6 @@ def load_config(path, dcs):
|
||||
for d in DCS_DEFAULTS:
|
||||
config.pop(d, None)
|
||||
config.update(dcs)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -646,3 +646,54 @@ def configure(config_file, dcs, namespace):
|
||||
config['dcs_api'] = str(dcs)
|
||||
config['namespace'] = str(namespace)
|
||||
store_config(config, config_file)
|
||||
|
||||
|
||||
def touch_member(config, dcs):
|
||||
''' Rip of the ha.touch_member without inter-class dependencies '''
|
||||
p = Postgresql(config['postgresql'])
|
||||
p.set_state('running')
|
||||
p.set_role('master')
|
||||
|
||||
api = RestApiServer(None, config['restapi'])
|
||||
data = {
|
||||
'conn_url': p.connection_string,
|
||||
'api_url': api.connection_string,
|
||||
'state': p.state,
|
||||
'role': p.role
|
||||
}
|
||||
try:
|
||||
dcs.touch_member(json.dumps(data, separators=(',', ':')), permanent=True)
|
||||
except DCSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def set_defaults(config, cluster_name):
|
||||
''' fill-in some basic configuration parameters if config file is not set '''
|
||||
config['postgresql']['name'] = config['postgresql'].get('name') or cluster_name
|
||||
config['postgresql']['scope'] = config['postgresql'].get('scope') or cluster_name
|
||||
config['postgresql']['listen'] = config['postgresql'].get('listen') or "127.0.0.1"
|
||||
config['postgresql']['authentication'] = {'replication': None}
|
||||
config['restapi']['listen'] = ':' in config['restapi'].get('listen', ".") or '127.0.0.1:5432'
|
||||
|
||||
|
||||
@ctl.command('scaffold', help='Create a structure for the cluster in DCS')
|
||||
@click.argument('cluster_name')
|
||||
@click.option('--sysid', '-s', help='System ID of the cluster to put into the initialize key', default="")
|
||||
@option_config_file
|
||||
@option_dcs
|
||||
def scaffold(cluster_name, config_file, dcs, sysid):
|
||||
logging.debug("config_file = %s, cluster_name = %s, dcs = %s, sysid = %s", config_file, cluster_name, dcs, sysid)
|
||||
config, dcs, cluster = ctl_load_config(cluster_name, config_file, dcs)
|
||||
if cluster and cluster.initialize:
|
||||
raise PatroniCtlException("This cluster is already initialized")
|
||||
dcs.initialize(create_new=True, sysid=sysid)
|
||||
|
||||
set_defaults(config, cluster_name)
|
||||
|
||||
# make sure the leader key will never expire
|
||||
if not (touch_member(config, dcs) and dcs.attempt_to_acquire_leader(permanent=True)):
|
||||
dcs.delete_leader()
|
||||
dcs.cancel_initialization()
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@@ -343,13 +343,14 @@ class AbstractDCS(object):
|
||||
"""Create or update `/config` key"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def touch_member(self, data, ttl=None):
|
||||
def touch_member(self, data, ttl=None, permanent=False):
|
||||
"""Update member key in DCS.
|
||||
This method should create or update key with the name = '/members/' + `~self._name`
|
||||
and value = data in a given DCS.
|
||||
|
||||
:param data: json serialized information about instance (including connection strings)
|
||||
:param ttl: ttl for member key, optional parameter. If it is None `~self.member_ttl will be used`
|
||||
:permanent: if set to `!True`, the member key will never expire. Used in patronictl for the external master.
|
||||
:returns: `!True` on success otherwise `!False`
|
||||
"""
|
||||
|
||||
|
||||
@@ -191,7 +191,8 @@ class Consul(AbstractDCS):
|
||||
return True
|
||||
|
||||
try:
|
||||
self._client.kv.put(self.member_path, data, acquire=self._session)
|
||||
args = {'cas': None} if kwargs.get('permanent') else {}
|
||||
self._client.kv.put(self.member_path, data, acquire=self._session, **args)
|
||||
self._my_member_data = data
|
||||
return True
|
||||
except Exception:
|
||||
@@ -199,8 +200,9 @@ class Consul(AbstractDCS):
|
||||
return False
|
||||
|
||||
@catch_consul_errors
|
||||
def attempt_to_acquire_leader(self):
|
||||
ret = self._client.kv.put(self.leader_path, self._name, acquire=self._session)
|
||||
def attempt_to_acquire_leader(self, permanent=False):
|
||||
args = {'cas': None} if permanent else {}
|
||||
ret = self._client.kv.put(self.leader_path, self._name, acquire=self._session, **args)
|
||||
if not ret:
|
||||
logger.info('Could not take out TTL lock')
|
||||
return ret
|
||||
|
||||
+8
-4
@@ -281,16 +281,20 @@ class Etcd(AbstractDCS):
|
||||
raise EtcdError('Etcd is not responding properly')
|
||||
|
||||
@catch_etcd_errors
|
||||
def touch_member(self, data, ttl=None):
|
||||
return self.retry(self._client.set, self.member_path, data, ttl or self._ttl)
|
||||
def touch_member(self, data, ttl=None, permanent=False):
|
||||
return self.retry(self._client.set, self.member_path, data, (ttl or self._ttl) if not permanent else None)
|
||||
|
||||
@catch_etcd_errors
|
||||
def take_leader(self):
|
||||
return self.retry(self._client.set, self.leader_path, self._name, self._ttl)
|
||||
|
||||
def attempt_to_acquire_leader(self):
|
||||
def attempt_to_acquire_leader(self, permanent=False):
|
||||
try:
|
||||
return bool(self.retry(self._client.write, self.leader_path, self._name, ttl=self._ttl, prevExist=False))
|
||||
return bool(self.retry(self._client.write,
|
||||
self.leader_path,
|
||||
self._name,
|
||||
ttl=self._ttl if permanent else None,
|
||||
prevExist=False))
|
||||
except etcd.EtcdAlreadyExist:
|
||||
logger.info('Could not take out TTL lock')
|
||||
except (RetryFailedError, etcd.EtcdException):
|
||||
|
||||
@@ -162,8 +162,11 @@ class ZooKeeper(AbstractDCS):
|
||||
except:
|
||||
return False
|
||||
|
||||
def attempt_to_acquire_leader(self):
|
||||
ret = self._create(self.leader_path, self._name, makepath=True, ephemeral=True)
|
||||
def attempt_to_acquire_leader(self, permanent=False):
|
||||
ret = self._create(self.leader_path,
|
||||
self._name,
|
||||
makepath=True,
|
||||
ephemeral=not permanent)
|
||||
if not ret:
|
||||
logger.info('Could not take out TTL lock')
|
||||
return ret
|
||||
@@ -192,7 +195,7 @@ class ZooKeeper(AbstractDCS):
|
||||
return self._create(self.initialize_path, sysid, makepath=True) if create_new \
|
||||
else self._client.retry(self._client.set, self.initialize_path, sysid.encode("utf-8"))
|
||||
|
||||
def touch_member(self, data, ttl=None):
|
||||
def touch_member(self, data, ttl=None, permanent=False):
|
||||
cluster = self.cluster
|
||||
member = cluster and ([m for m in cluster.members if m.name == self._name] or [None])[0]
|
||||
path = self.member_path
|
||||
@@ -213,7 +216,7 @@ class ZooKeeper(AbstractDCS):
|
||||
if member:
|
||||
self._client.retry(self._client.set, path, data)
|
||||
else:
|
||||
self._client.retry(self._client.create, path, data, makepath=True, ephemeral=True)
|
||||
self._client.retry(self._client.create, path, data, makepath=True, ephemeral=not permanent)
|
||||
self._my_member_data = data
|
||||
return True
|
||||
except NodeExistsError:
|
||||
|
||||
Reference in New Issue
Block a user