Compare commits

...
17 Commits
Author SHA1 Message Date
Alexander KukushkinandGitHub f500dbb0ff Release 1.4.3 (#635)
Bump version and update release notes
2018-03-05 10:10:17 +01:00
Andy NewtonandAlexander Kukushkin f748de3b29 Make log level configurable from environment variables (#622)
* `PATRONI_LOGLEVEL` - sets the general logging level
* `PATRONI_REQUESTS_LOGLEVEL` - sets the logging level for all HTTP requests e.g. Kubernetes API calls
2018-03-05 09:50:45 +01:00
Alexander KukushkinandGitHub 3afd26101b Single user mode was waiting for user input and never finish (#634)
Regression was introduced in https://github.com/zalando/patroni/pull/576
2018-03-02 22:22:43 +01:00
Alexander KukushkinandGitHub c04e7a1798 Write bootstrap.pg_hba into a pg_hba.conf after custom bootstrap (#632)
Fixes https://github.com/zalando/patroni/issues/631
2018-02-26 18:48:56 +01:00
Alexander KukushkinandGitHub 89a11fed07 Don't rediscover etcd cluster topology when watch timed out (#630)
but switch to the next node if it is possible.

Fixes https://github.com/zalando/patroni/issues/628
2018-02-26 18:48:30 +01:00
Alexander KukushkinandGitHub c95dd990cc Release 1.4.2 (#619)
* Bump version to 1.4.2
* Update release notes
2018-01-30 16:44:42 +01:00
Alexander KukushkinandGitHub dd1500b4dc Handle exceptions raised from psutil (#610)
Process.cmdline can raise `NoSuchProcess` or `AccessDenied`
Process.children can raise `AccessDenied`

Fixes https://github.com/zalando/patroni/issues/609
2018-01-30 16:26:39 +01:00
Alexander KukushkinandGitHub 0db788bb95 Make show-config work with cluster_name from config file (#618)
similar to edit-config, list and so on
2018-01-30 14:24:54 +01:00
Maciej SzulikandAlexander Kukushkin a4aaf53212 Add proper rbac to run patroni on k8s (#616)
Adds 3 resources that will properly setup the RBAC:

1. service account, which is also assigned to the pods of the cluster, so that they use those particular permissions 
2. a role, which holds only the necessary permissions that patroni members need to interact with k8s cluster
3. a rolebinding, which connects two two former things together to use.

The role and rolebinding was created using this tool https://github.com/liggitt/audit2rbac which looks at [audit logs](https://kubernetes.io/docs/tasks/debug-application-cluster/audit/#advanced-audit) provided  by the api server.
2018-01-30 12:00:49 +01:00
Maciej SzulikandOleksii Kliukin 3d293ac087 Change the pip definition in Dockerfile to use master now (#617) 2018-01-30 10:58:08 +01:00
Alexander KukushkinandGitHub a0c8491abb Don't swallow silently all errors from k8s API (#611)
Output exception trace to the logs when http status code == 403, something is wrong with permissions.

When http status code == 409 -- such error could be ignored, because object probably was created or updated by another process.

For all other http status codes it will also produce stack traces.

I hope it will help to debug issues similar to the https://github.com/zalando/patroni/issues/606
2018-01-26 09:57:17 +01:00
Alexander KukushkinandGitHub f8b6b21297 Avoid calling pg_controldata during bootstrap (#612)
Fixes https://github.com/zalando/patroni/issues/608
2018-01-26 09:56:17 +01:00
Alexander KukushkinandGitHub 9825b8a584 Improve patronictl list UX (#613)
* rename scheduled failover to scheduled switchover
* show information about pending_restarts
2018-01-26 09:55:54 +01:00
Alexander KukushkinandGitHub 8da05ad785 Update haproxy.tmpl (#614)
connection to postgres should be closed forcibly when health-check fails
2018-01-25 15:31:02 +01:00
Alexander KukushkinandGitHub a1e5c8e1cb A few iprovements in patronictl (#601)
* make switchover work with an old patroni
* exclude leader from candidates when interactively running failover
2018-01-17 15:33:08 +01:00
Oleksii KliukinandGitHub 4202ad853a Minor corrections to the documentation. (#599) 2018-01-10 16:10:12 +01:00
Alexander KukushkinandGitHub 93ac309b38 Fix link to the Kubernetes documentation (#598)
blog => blob
2018-01-10 13:19:23 +01:00
19 changed files with 277 additions and 74 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ Patroni is a template for you to create your own customized, high-availability s
We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely. We call Patroni a "template" because it is far from being a one-size-fits-all or plug-and-play replication system. It will have its own caveats. Use wisely.
**Note to Kubernetes users**: Patroni can run natively on top of Kubernetes. Take a look at the `Kubernetes <https://github.com/zalando/patroni/blog/master/docs/kubernetes.rst>`__ chapter of the Patroni documentation. **Note to Kubernetes users**: Patroni can run natively on top of Kubernetes. Take a look at the `Kubernetes <https://github.com/zalando/patroni/blob/master/docs/kubernetes.rst>`__ chapter of the Patroni documentation.
.. contents:: .. contents::
:local: :local:
+3 -1
View File
@@ -11,6 +11,8 @@ Global/Universal
- **PATRONI\_NAME**: name of the node where the current instance of Patroni is running. Must be unique for the cluster. - **PATRONI\_NAME**: name of the node where the current instance of Patroni is running. Must be unique for the cluster.
- **PATRONI\_NAMESPACE**: path within the configuration store where Patroni will keep information about the cluster. Default value: "/service" - **PATRONI\_NAMESPACE**: path within the configuration store where Patroni will keep information about the cluster. Default value: "/service"
- **PATRONI\_SCOPE**: cluster name - **PATRONI\_SCOPE**: cluster name
- **PATRONI\_LOGLEVEL**: sets the general logging level (see `the docs for Python logging <https://docs.python.org/3.6/library/logging.html#levels>`_)
- **PATRONI\_REQUESTS_LOGLEVEL**: sets the logging level for all HTTP requests e.g. Kubernetes API calls (see `the docs for Python logging <https://docs.python.org/3.6/library/logging.html#levels>`_)
Bootstrap configuration Bootstrap configuration
----------------------- -----------------------
@@ -55,7 +57,7 @@ Exhibitor
Kubernetes Kubernetes
---------- ----------
- **PATRONI\_KUBERNETES\_NAMESPACE**: (optional) Kubernetes namespace where the operator is running. Default value is `default`. - **PATRONI\_KUBERNETES\_NAMESPACE**: (optional) Kubernetes namespace where the Patroni pod is running. Default value is `default`.
- **PATRONI\_KUBERNETES\_LABELS**: Labels in format ``{label1: value1, label2: value2}``. These labels will be used to find existing objects (Pods and either Endpoints or ConfigMaps) associated with the current cluster. Also Patroni will set them on every object (Endpoint or ConfigMap) it creates. - **PATRONI\_KUBERNETES\_LABELS**: Labels in format ``{label1: value1, label2: value2}``. These labels will be used to find existing objects (Pods and either Endpoints or ConfigMaps) associated with the current cluster. Also Patroni will set them on every object (Endpoint or ConfigMap) it creates.
- **PATRONI\_KUBERNETES\_SCOPE\_LABEL**: (optional) name of the label containing cluster name. Default value is `cluster-name`. - **PATRONI\_KUBERNETES\_SCOPE\_LABEL**: (optional) name of the label containing cluster name. Default value is `cluster-name`.
- **PATRONI\_KUBERNETES\_ROLE\_LABEL**: (optional) name of the label containing Postgres role (`master` or `replica`). Patroni will set this label on the pod it is running in. Default value is `role`. - **PATRONI\_KUBERNETES\_ROLE\_LABEL**: (optional) name of the label containing Postgres role (`master` or `replica`). Patroni will set this label on the pod it is running in. Default value is `role`.
+1 -1
View File
@@ -87,7 +87,7 @@ Exhibitor
Kubernetes Kubernetes
---------- ----------
- **namespace**: (optional) Kubernetes namespace where we are running. Default value is `default`. - **namespace**: (optional) Kubernetes namespace where Patroni pod is running. Default value is `default`.
- **labels**: Labels in format ``{label1: value1, label2: value2}``. These labels will be used to find existing objects (Pods and either Endpoints or ConfigMaps) associated with the current cluster. Also Patroni will set them on every object (Endpoint or ConfigMap) it creates. - **labels**: Labels in format ``{label1: value1, label2: value2}``. These labels will be used to find existing objects (Pods and either Endpoints or ConfigMaps) associated with the current cluster. Also Patroni will set them on every object (Endpoint or ConfigMap) it creates.
- **scope\_label**: (optional) name of the label containing cluster name. Default value is `cluster-name`. - **scope\_label**: (optional) name of the label containing cluster name. Default value is `cluster-name`.
- **role\_label**: (optional) name of the label containing role (master or replica). Patroni will set this label on the pod it runs in. Default value is ``role``. - **role\_label**: (optional) name of the label containing role (master or replica). Patroni will set this label on the pod it runs in. Default value is ``role``.
+83
View File
@@ -3,6 +3,89 @@
Release notes Release notes
============= =============
Version 1.4.3
-------------
**Improvements in logging**
- Make log level configurable from environment variables (Andy Newton, Keyvan Hedayati)
`PATRONI_LOGLEVEL` - sets the general logging level
`PATRONI_REQUESTS_LOGLEVEL` - sets the logging level for all HTTP requests e.g. Kubernetes API calls
See `the docs for Python logging <https://docs.python.org/3.6/library/logging.html#levels>` to get the names of possible log levels
**Stability improvements and bug fixes**
- Don't rediscover etcd cluster topology when watch timed out (Alexander Kukushkin)
If we have only one host in etcd configuration and exactly this host is not accessible, Patroni was starting discovery of cluster topology and never succeeding. Instead it should just switch to the next available node.
- Write content of bootstrap.pg_hba into a pg_hba.conf after custom bootstrap (Alexander)
Now it behaves similarly to the usual bootstrap with `initdb`
- Single user mode was waiting for user input and never finish (Alexander)
Regression was introduced in https://github.com/zalando/patroni/pull/576
Version 1.4.2
-------------
**Improvements in patronictl**
- Rename scheduled failover to scheduled switchover (Alexander Kukushkin)
Failover and switchover functions were separated in version 1.4, but `patronictl list` was still reporting `Scheduled failover` instead of `Scheduled switchover`.
- Show information about pending restarts (Alexander)
In order to apply some configuration changes sometimes it is necessary to restart postgres. Patroni was already giving a hint about that in the REST API and when writing node status into DCS, but there were no easy way to display it.
- Make show-config to work with cluster_name from config file (Alexander)
It works similar to the `patronictl edit-config`
**Stability improvements**
- Avoid calling pg_controldata during bootstrap (Alexander)
During initdb or custom bootstrap there is a time window when pgdata is not empty but pg_controldata has not been written yet. In such case pg_controldata call was failing with error messages.
- Handle exceptions raised from psutil (Alexander)
cmdline is read and parsed every time when `cmdline()` method is called. It could happen that the process being examined
has already disappeared, in that case `NoSuchProcess` is raised.
**Kubernetes support improvements**
- Don't swallow errors from k8s API (Alexander)
A call to Kubernetes API could fail for a different number of reasons. In some cases such call should be retried, in some other cases we should log the error message and the exception stack trace. The change here will help debug Kubernetes permission issues.
- Update Kubernetes example Dockerfile to install Patroni from the master branch (Maciej Szulik)
Before that it was using `feature/k8s`, which became outdated.
- Add proper RBAC to run patroni on k8s (Maciej)
Add the Service account that is assigned to the pods of the cluster, the role that holds only the necessary permissions, and the rolebinding that connects the Service account and the Role.
Version 1.4.1
-------------
**Fixes in patronictl**
- Don't show current leader in suggested list of members to failover to. (Alexander Kukushkin)
patronictl failover could still work when there is leader in the cluster and it should be excluded from the list of member where it is possible to failover to.
- Make patronictl switchover compatible with the old Patroni api (Alexander)
In case if POST /switchover REST API call has failed with status code 501 it will do it once again, but to /failover endpoint.
Version 1.4 Version 1.4
----------- -----------
+13 -9
View File
@@ -10,19 +10,23 @@ defaults
timeout server 30m timeout server 30m
timeout check 5s timeout check 5s
frontend master_postgresql listen stats
mode http
bind *:7000
stats enable
stats uri /
listen master
bind *:5000 bind *:5000
default_backend backend_master
frontend replicas_postgresql
bind *:5001
default_backend backend_replicas
backend backend_master
option httpchk OPTIONS /master option httpchk OPTIONS /master
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
{{range gets "/members/*"}} server {{base .Key}} {{$data := json .Value}}{{base (replace (index (split $data.conn_url "/") 2) "@" "/" -1)}} maxconn 100 check port {{index (split (index (split $data.api_url "/") 2) ":") 1}} {{range gets "/members/*"}} server {{base .Key}} {{$data := json .Value}}{{base (replace (index (split $data.conn_url "/") 2) "@" "/" -1)}} maxconn 100 check port {{index (split (index (split $data.api_url "/") 2) ":") 1}}
{{end}} {{end}}
backend backend_replicas listen replicas
bind *:5001
option httpchk OPTIONS /replica option httpchk OPTIONS /replica
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
{{range gets "/members/*"}} server {{base .Key}} {{$data := json .Value}}{{base (replace (index (split $data.conn_url "/") 2) "@" "/" -1)}} maxconn 100 check port {{index (split (index (split $data.api_url "/") 2) ":") 1}} {{range gets "/members/*"}} server {{base .Key}} {{$data := json .Value}}{{base (replace (index (split $data.conn_url "/") 2) "@" "/" -1)}} maxconn 100 check port {{index (split (index (split $data.api_url "/") 2) ":") 1}}
{{end}} {{end}}
+1 -1
View File
@@ -12,7 +12,7 @@ RUN export DEBIAN_FRONTEND=noninteractive \
&& localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \ && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
&& pip install setuptools pip --upgrade \ && pip install setuptools pip --upgrade \
&& pip install 'git+https://github.com/zalando/patroni.git@feature/k8s#egg=patroni[kubernetes]' \ && pip install 'git+https://github.com/zalando/patroni.git#egg=patroni[kubernetes]' \
&& mkdir -p /home/postgres \ && mkdir -p /home/postgres \
&& chown postgres:postgres /home/postgres \ && chown postgres:postgres /home/postgres \
+60
View File
@@ -14,6 +14,7 @@ spec:
application: patroni application: patroni
cluster-name: *cluster_name cluster-name: *cluster_name
spec: spec:
serviceAccountName: patronidemo
containers: containers:
- name: *cluster_name - name: *cluster_name
image: patroni # docker build -t patroni . image: patroni # docker build -t patroni .
@@ -120,3 +121,62 @@ type: Opaque
data: data:
superuser-password: emFsYW5kbw== superuser-password: emFsYW5kbw==
replication-password: cmVwLXBhc3M= replication-password: cmVwLXBhc3M=
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: patronidemo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: patronidemo
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
- patch
- update
# the following three privileges are necessary only when using endpoints
- create
- list
- watch
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- list
- patch
- update
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: patronidemo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: patronidemo
subjects:
- kind: ServiceAccount
name: patronidemo
+4 -2
View File
@@ -135,8 +135,10 @@ class Patroni(object):
def patroni_main(): def patroni_main():
logformat = os.environ.get('PATRONI_LOGFORMAT', '%(asctime)s %(levelname)s: %(message)s') logformat = os.environ.get('PATRONI_LOGFORMAT', '%(asctime)s %(levelname)s: %(message)s')
logging.basicConfig(format=logformat, level=logging.INFO) loglevel = os.environ.get('PATRONI_LOGLEVEL', 'INFO')
logging.getLogger('requests').setLevel(logging.WARNING) requests_loglevel = os.environ.get('PATRONI_REQUESTS_LOGLEVEL', 'WARNING')
logging.basicConfig(format=logformat, level=loglevel)
logging.getLogger('requests').setLevel(requests_loglevel)
patroni = Patroni() patroni = Patroni()
try: try:
+27 -17
View File
@@ -546,15 +546,14 @@ def _do_failover_or_switchover(obj, action, cluster_name, master, candidate, for
dcs = get_dcs(obj, cluster_name) dcs = get_dcs(obj, cluster_name)
cluster = dcs.get_cluster() cluster = dcs.get_cluster()
if action == 'switchover': if action == 'switchover' and cluster.leader is None:
if cluster.leader is None: raise PatroniCtlException('This cluster has no master')
raise PatroniCtlException('This cluster has no master')
if master is None: if master is None:
if force: if force or action == 'failover':
master = cluster.leader.member.name master = cluster.leader and cluster.leader.name
else: else:
master = click.prompt('Master', type=str, default=cluster.leader.member.name) master = click.prompt('Master', type=str, default=cluster.leader.member.name)
if master is not None and cluster.leader and cluster.leader.member.name != master: if master is not None and cluster.leader and cluster.leader.member.name != master:
raise PatroniCtlException('Member {0} is not the leader of cluster {1}'.format(master, cluster_name)) raise PatroniCtlException('Member {0} is not the leader of cluster {1}'.format(master, cluster_name))
@@ -610,6 +609,11 @@ def _do_failover_or_switchover(obj, action, cluster_name, master, candidate, for
member = cluster.leader.member if cluster.leader else cluster.get_member(candidate, False) member = cluster.leader.member if cluster.leader else cluster.get_member(candidate, False)
r = request_patroni(member, 'post', action, failover_value, auth_header(obj)) r = request_patroni(member, 'post', action, failover_value, auth_header(obj))
# probably old patroni, which doesn't support switchover yet
if r.status_code == 501 and action == 'switchover' and 'Server does not support this operation' in r.text:
r = request_patroni(member, 'post', 'failover', failover_value, auth_header(obj))
if r.status_code in (200, 202): if r.status_code in (200, 202):
logging.debug(r) logging.debug(r)
cluster = dcs.get_cluster() cluster = dcs.get_cluster()
@@ -662,7 +666,8 @@ def output_members(cluster, name, extended=False, fmt='pretty'):
# Mainly for consistent pretty printing and watching we sort the output # Mainly for consistent pretty printing and watching we sort the output
cluster.members.sort(key=lambda x: x.name) cluster.members.sort(key=lambda x: x.name)
extended = extended or any(m.data.get('scheduled_restart') for m in cluster.members) has_scheduled_restarts = any(m.data.get('scheduled_restart') for m in cluster.members)
has_pending_restarts = any(m.data.get('pending_restart') for m in cluster.members)
for m in cluster.members: for m in cluster.members:
logging.debug(m) logging.debug(m)
@@ -682,7 +687,10 @@ def output_members(cluster, name, extended=False, fmt='pretty'):
row = [name, m.name, m.conn_kwargs()['host'], role, m.data.get('state', ''), lag] row = [name, m.name, m.conn_kwargs()['host'], role, m.data.get('state', ''), lag]
if extended: if extended or has_pending_restarts:
row.append('*' if m.data.get('pending_restart') else '')
if extended or has_scheduled_restarts:
value = '' value = ''
scheduled_restart = m.data.get('scheduled_restart') scheduled_restart = m.data.get('scheduled_restart')
if scheduled_restart: if scheduled_restart:
@@ -695,11 +703,13 @@ def output_members(cluster, name, extended=False, fmt='pretty'):
rows.append(row) rows.append(row)
columns = ['Cluster', 'Member', 'Host', 'Role', 'State', 'Lag in MB'] columns = ['Cluster', 'Member', 'Host', 'Role', 'State', 'Lag in MB']
alignment = {'Cluster': 'l', 'Member': 'l', 'Host': 'l', 'Lag in MB': 'r'} alignment = {'Lag in MB': 'r'}
if extended: if extended or has_pending_restarts:
columns.append('Pending restart')
if extended or has_scheduled_restarts:
columns.append('Scheduled restart') columns.append('Scheduled restart')
alignment['Scheduled restart'] = 'l'
print_output(columns, rows, alignment, fmt) print_output(columns, rows, alignment, fmt)
@@ -708,11 +718,11 @@ def output_members(cluster, name, extended=False, fmt='pretty'):
service_info.append('Maintenance mode: on') service_info.append('Maintenance mode: on')
if cluster.failover and cluster.failover.scheduled_at: if cluster.failover and cluster.failover.scheduled_at:
info = 'Failover scheduled at: ' + cluster.failover.scheduled_at.isoformat() info = 'Switchover scheduled at: ' + cluster.failover.scheduled_at.isoformat()
if cluster.failover.leader: if cluster.failover.leader:
info += '\n from: ' + cluster.failover.leader info += '\n from: ' + cluster.failover.leader
if cluster.failover.candidate: if cluster.failover.candidate:
info += '\n to: ' + cluster.failover.candidate info += '\n to: ' + cluster.failover.candidate
service_info.append(info) service_info.append(info)
if service_info: if service_info:
@@ -1091,7 +1101,7 @@ def edit_config(obj, cluster_name, force, quiet, kvpairs, pgkvpairs, apply_filen
@ctl.command('show-config', help="Show cluster configuration") @ctl.command('show-config', help="Show cluster configuration")
@click.argument('cluster_name') @arg_cluster_name
@click.pass_obj @click.pass_obj
def show_config(obj, cluster_name): def show_config(obj, cluster_name):
cluster = get_dcs(obj, cluster_name).get_cluster() cluster = get_dcs(obj, cluster_name).get_cluster()
+5 -2
View File
@@ -210,8 +210,11 @@ class Client(etcd.Client):
self._machines_cache = self.machines self._machines_cache = self.machines
if self._base_uri in self._machines_cache: if self._base_uri in self._machines_cache:
self._machines_cache.remove(self._base_uri) self._machines_cache.remove(self._base_uri)
except etcd.EtcdConnectionFailed: except etcd.EtcdConnectionFailed as e:
self._update_machines_cache = True if isinstance(e, etcd.EtcdWatchTimedOut) and self._machines_cache:
self._base_uri = self._next_server()
else:
self._update_machines_cache = True
if not response: if not response:
raise raise
return self._handle_server_response(response) return self._handle_server_response(response)
+17 -13
View File
@@ -29,7 +29,7 @@ class KubernetesRetriableException(k8s_client.rest.ApiException):
self.headers = orig.headers self.headers = orig.headers
class CoreV1Api(object): class CoreV1ApiProxy(object):
def __init__(self, use_endpoints=False): def __init__(self, use_endpoints=False):
self._api = k8s_client.CoreV1Api() self._api = k8s_client.CoreV1Api()
@@ -55,6 +55,21 @@ class CoreV1Api(object):
return wrapper return wrapper
def catch_kubernetes_errors(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except k8s_client.rest.ApiException as e:
if e.status == 403:
logger.exception('Permission denied')
elif e.status != 409: # Object exists or conflict in resource_version
logger.exception('Unexpected error from Kubernetes API')
return False
except (RetryFailedError, HTTPException, HTTPError, socket.error, socket.timeout):
return False
return wrapper
class Kubernetes(AbstractDCS): class Kubernetes(AbstractDCS):
def __init__(self, config): def __init__(self, config):
@@ -84,7 +99,7 @@ class Kubernetes(AbstractDCS):
port.update({n: p[n] for n in ('name', 'protocol') if p.get(n)}) port.update({n: p[n] for n in ('name', 'protocol') if p.get(n)})
ports.append(k8s_client.V1EndpointPort(**port)) ports.append(k8s_client.V1EndpointPort(**port))
self.__subsets = [k8s_client.V1EndpointSubset(addresses=addresses, ports=ports)] self.__subsets = [k8s_client.V1EndpointSubset(addresses=addresses, ports=ports)]
self._api = CoreV1Api(use_endpoints) self._api = CoreV1ApiProxy(use_endpoints)
self.set_retry_timeout(config['retry_timeout']) self.set_retry_timeout(config['retry_timeout'])
self.set_ttl(config.get('ttl') or 30) self.set_ttl(config.get('ttl') or 30)
self._leader_observed_record = {} self._leader_observed_record = {}
@@ -96,16 +111,6 @@ class Kubernetes(AbstractDCS):
def retry(self, *args, **kwargs): def retry(self, *args, **kwargs):
return self._retry.copy()(*args, **kwargs) return self._retry.copy()(*args, **kwargs)
def catch_kubernetes_errors(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except (RetryFailedError, k8s_client.rest.ApiException,
HTTPException, HTTPError, socket.error, socket.timeout):
return False
return wrapper
def client_path(self, path): def client_path(self, path):
return super(Kubernetes, self).client_path(path)[1:].replace('/', '-') return super(Kubernetes, self).client_path(path)[1:].replace('/', '-')
@@ -362,7 +367,6 @@ class Kubernetes(AbstractDCS):
def delete_cluster(self): def delete_cluster(self):
self.retry(self._api.delete_collection_namespaced_kind, self._namespace, label_selector=self._label_selector) self.retry(self._api.delete_collection_namespaced_kind, self._namespace, label_selector=self._label_selector)
@catch_kubernetes_errors
def set_history_value(self, value): def set_history_value(self, value):
patch = bool(self.cluster and self.cluster.config and self.cluster.config.index) patch = bool(self.cluster and self.cluster.config and self.cluster.config.index)
return self.patch_or_create(self.config_path, {self._HISTORY: value}, None, patch, False) return self.patch_or_create(self.config_path, {self._HISTORY: value}, None, patch, False)
+24 -16
View File
@@ -537,16 +537,12 @@ class Postgresql(object):
ret = self.pg_ctl('initdb', *options) ret = self.pg_ctl('initdb', *options)
if pwfile: if pwfile:
os.remove(pwfile) os.remove(pwfile)
if ret: if not ret:
if not self._server_parameters.get('hba_file') and not self.config.get('pg_hba'):
self.write_pg_hba(config.get('pg_hba', []))
self._major_version = self.get_major_version()
self._server_parameters = self.get_server_parameters(self.config)
else:
self.set_state('initdb failed') self.set_state('initdb failed')
return ret return ret
def _custom_bootstrap(self, config): def _custom_bootstrap(self, config):
self.set_state('running custom bootstrap script')
params = ['--scope=' + self.scope, '--datadir=' + self._data_dir] params = ['--scope=' + self.scope, '--datadir=' + self._data_dir]
try: try:
logger.info('Running custom bootstrap script: %s', config['command']) logger.info('Running custom bootstrap script: %s', config['command'])
@@ -557,7 +553,6 @@ class Postgresql(object):
logger.exception('Exception during custom bootstrap') logger.exception('Exception during custom bootstrap')
return False return False
self._post_restore() self._post_restore()
self.save_configuration_files()
if 'recovery_conf' in config: if 'recovery_conf' in config:
self.write_recovery_conf(config['recovery_conf']) self.write_recovery_conf(config['recovery_conf'])
@@ -1079,8 +1074,10 @@ class Postgresql(object):
return True return True
def write_pg_hba(self, config): def write_pg_hba(self, config):
with open(self._pg_hba_conf, 'a') as f: if not self._server_parameters.get('hba_file') and not self.config.get('pg_hba'):
f.write('\n{}\n'.format('\n'.join(config))) with open(self._pg_hba_conf, 'a') as f:
f.write('\n{}\n'.format('\n'.join(config)))
return True
def _replace_pg_hba(self): def _replace_pg_hba(self):
""" """
@@ -1161,7 +1158,7 @@ class Postgresql(object):
""" return the contents of pg_controldata, or non-True value if pg_controldata call failed """ """ return the contents of pg_controldata, or non-True value if pg_controldata call failed """
result = {} result = {}
# Don't try to call pg_controldata during backup restore # Don't try to call pg_controldata during backup restore
if self._version_file_exists() and self.state != 'creating replica': if not self.bootstrapping and self._version_file_exists() and self.state != 'creating replica':
try: try:
data = subprocess.check_output([self._pgcommand('pg_controldata'), self._data_dir], data = subprocess.check_output([self._pgcommand('pg_controldata'), self._data_dir],
env={'LANG': 'C', 'LC_ALL': 'C', 'PATH': os.environ['PATH']}) env={'LANG': 'C', 'LC_ALL': 'C', 'PATH': os.environ['PATH']})
@@ -1397,6 +1394,7 @@ class Postgresql(object):
shutil.copy(config_file, backup_file) shutil.copy(config_file, backup_file)
except IOError: except IOError:
logger.exception('unable to create backup copies of configuration files') logger.exception('unable to create backup copies of configuration files')
return True
def restore_configuration_files(self): def restore_configuration_files(self):
""" restore a previously saved postgresql.conf """ """ restore a previously saved postgresql.conf """
@@ -1546,6 +1544,7 @@ $$""".format(name, ' '.join(options)), name, password, password)
def bootstrap(self, config): def bootstrap(self, config):
""" Initialize a new node from scratch and start it. """ """ Initialize a new node from scratch and start it. """
pg_hba = config.get('pg_hba', [])
method = config.get('method') or 'initdb' method = config.get('method') or 'initdb'
self._running_custom_bootstrap = method != 'initdb' and method in config and 'command' in config[method] self._running_custom_bootstrap = method != 'initdb' and method in config and 'command' in config[method]
if self._running_custom_bootstrap: if self._running_custom_bootstrap:
@@ -1553,7 +1552,8 @@ $$""".format(name, ' '.join(options)), name, password, password)
config = config[method] config = config[method]
else: else:
do_initialize = self._initdb do_initialize = self._initdb
return do_initialize(config) and self._configure_server_parameters() and self.start() return do_initialize(config) and self.write_pg_hba(pg_hba) and self.save_configuration_files() \
and self._configure_server_parameters() and self.start()
def post_bootstrap(self, config, task): def post_bootstrap(self, config, task):
try: try:
@@ -1790,10 +1790,20 @@ $$""".format(name, ' '.join(options)), name, password, password)
return self.single_user_mode(options=opts) == 0 or None return self.single_user_mode(options=opts) == 0 or None
def cancellable_subprocess_call(self, *args, **kwargs): def cancellable_subprocess_call(self, *args, **kwargs):
communicate_input = kwargs.pop('communicate_input', None)
for s in ('stdin', 'stdout', 'stderr'): for s in ('stdin', 'stdout', 'stderr'):
kwargs.pop(s, None) kwargs.pop(s, None)
communicate_input = 'communicate_input' in kwargs
if communicate_input:
input_data = kwargs.pop('communicate_input', None)
if not isinstance(input_data, string_types):
input_data = ''
if input_data and input_data[-1] != '\n':
input_data += '\n'
kwargs['stdin'] = subprocess.PIPE
kwargs['stdout'] = open(os.devnull, 'w')
kwargs['stderr'] = subprocess.STDOUT
try: try:
with self._cancellable_lock: with self._cancellable_lock:
if self._is_cancelled: if self._is_cancelled:
@@ -1803,10 +1813,8 @@ $$""".format(name, ' '.join(options)), name, password, password)
self._cancellable = subprocess.Popen(*args, **kwargs) self._cancellable = subprocess.Popen(*args, **kwargs)
if communicate_input: if communicate_input:
kwargs['stdin'] = subprocess.PIPE if input_data:
if communicate_input[-1] != '\n': self._cancellable.communicate(input_data)
communicate_input += '\n'
self._cancellable.communicate(communicate_input + '\n')
self._cancellable.stdin.close() self._cancellable.stdin.close()
return self._cancellable.wait() return self._cancellable.wait()
+17 -5
View File
@@ -82,11 +82,23 @@ class PostmasterProcess(psutil.Process):
"autovacuum launcher|autovacuum worker|stats collector|wal receiver|archiver|" "autovacuum launcher|autovacuum worker|stats collector|wal receiver|archiver|"
"wal sender) process|bgworker: )") "wal sender) process|bgworker: )")
user_backends = [p for p in self.children() if not aux_proc_re.match(p.cmdline()[0])] try:
logger.debug("Waiting for user backends {0} to close".format( user_backends = []
",".join(p.cmdline()[0] for p in user_backends))) user_backends_cmdlines = []
psutil.wait_procs(user_backends) for child in self.children():
logger.debug("Backends closed") try:
cmdline = child.cmdline()[0]
if not aux_proc_re.match(cmdline):
user_backends.append(child)
user_backends_cmdlines.append(cmdline)
except psutil.NoSuchProcess:
pass
if user_backends:
logger.debug('Waiting for user backends %s to close', ', '.join(user_backends_cmdlines))
psutil.wait_procs(user_backends)
logger.debug("Backends closed")
except psutil.Error:
logger.exception('wait_for_user_backends_to_close')
@classmethod @classmethod
def start(cls, pgcommand, data_dir, conf, options): def start(cls, pgcommand, data_dir, conf, options):
+1 -1
View File
@@ -1 +1 @@
__version__ = '1.4' __version__ = '1.4.3'
+5
View File
@@ -129,6 +129,11 @@ class TestCtl(unittest.TestCase):
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny') result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny')
assert 'Switchover failed' in result.output assert 'Switchover failed' in result.output
mocked.return_value.status_code = 501
mocked.return_value.text = 'Server does not support this operation'
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny')
assert 'Switchover failed' in result.output
# No members available # No members available
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_only_leader mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_only_leader
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny') result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny')
+1
View File
@@ -207,6 +207,7 @@ class TestClient(unittest.TestCase):
mock_machines.__get__ = Mock(return_value=['http://localhost:2379']) mock_machines.__get__ = Mock(return_value=['http://localhost:2379'])
self.client._machines_cache_updated = 0 self.client._machines_cache_updated = 0
self.client.api_execute('/', 'POST', timeout=0) self.client.api_execute('/', 'POST', timeout=0)
self.client._machines_cache = [self.client._base_uri]
self.assertRaises(etcd.EtcdWatchTimedOut, self.client.api_execute, '/timeout', 'POST', params={'wait': 'true'}) self.assertRaises(etcd.EtcdWatchTimedOut, self.client.api_execute, '/timeout', 'POST', params={'wait': 'true'})
self.assertRaises(etcd.EtcdException, self.client.api_execute, '/', '') self.assertRaises(etcd.EtcdException, self.client.api_execute, '/', '')
self.client._update_machines_cache = True self.client._update_machines_cache = True
+4 -3
View File
@@ -1,7 +1,7 @@
import unittest import unittest
from mock import Mock, patch from mock import Mock, patch
from patroni.dcs.kubernetes import Kubernetes, KubernetesError, k8s_client, k8s_watch from patroni.dcs.kubernetes import Kubernetes, KubernetesError, k8s_client, k8s_watch, RetryFailedError
def mock_list_namespaced_config_map(self, *args, **kwargs): def mock_list_namespaced_config_map(self, *args, **kwargs):
@@ -57,7 +57,8 @@ class TestKubernetes(unittest.TestCase):
self.k.take_leader() self.k.take_leader()
def test_manual_failover(self): def test_manual_failover(self):
self.k.manual_failover('foo', 'bar') with patch.object(k8s_client.CoreV1Api, 'patch_namespaced_config_map', Mock(side_effect=RetryFailedError(''))):
self.k.manual_failover('foo', 'bar')
def test_set_config_value(self): def test_set_config_value(self):
self.k.set_config_value('{}') self.k.set_config_value('{}')
@@ -79,7 +80,7 @@ class TestKubernetes(unittest.TestCase):
self.k.cancel_initialization() self.k.cancel_initialization()
@patch.object(k8s_client.CoreV1Api, 'delete_collection_namespaced_config_map', @patch.object(k8s_client.CoreV1Api, 'delete_collection_namespaced_config_map',
Mock(side_effect=k8s_client.rest.ApiException(500, ''))) Mock(side_effect=k8s_client.rest.ApiException(403, '')))
def test_delete_cluster(self): def test_delete_cluster(self):
self.k.delete_cluster() self.k.delete_cluster()
+2 -1
View File
@@ -567,6 +567,7 @@ class TestPostgresql(unittest.TestCase):
@patch.object(Postgresql, 'cancellable_subprocess_call') @patch.object(Postgresql, 'cancellable_subprocess_call')
def test_custom_bootstrap(self, mock_cancellable_subprocess_call): def test_custom_bootstrap(self, mock_cancellable_subprocess_call):
self.p.config.pop('pg_hba')
config = {'method': 'foo', 'foo': {'command': 'bar'}} config = {'method': 'foo', 'foo': {'command': 'bar'}}
mock_cancellable_subprocess_call.return_value = 1 mock_cancellable_subprocess_call.return_value = 1
@@ -945,7 +946,7 @@ class TestPostgresql(unittest.TestCase):
def test_cancellable_subprocess_call(self): def test_cancellable_subprocess_call(self):
self.p.cancel() self.p.cancel()
self.assertRaises(PostgresException, self.p.cancellable_subprocess_call) self.assertRaises(PostgresException, self.p.cancellable_subprocess_call, communicate_input=None)
@patch('patroni.postgresql.polling_loop', Mock(return_value=[0, 0])) @patch('patroni.postgresql.polling_loop', Mock(return_value=[0, 0]))
def test_cancel(self): def test_cancel(self):
+8 -1
View File
@@ -61,11 +61,18 @@ class TestPostmasterProcess(unittest.TestCase):
c1.cmdline = Mock(return_value=["postgres: startup process"]) c1.cmdline = Mock(return_value=["postgres: startup process"])
c2 = Mock() c2 = Mock()
c2.cmdline = Mock(return_value=["postgres: postgres postgres [local] idle"]) c2.cmdline = Mock(return_value=["postgres: postgres postgres [local] idle"])
with patch('psutil.Process.children', Mock(return_value=[c1, c2])): c3 = Mock()
c3.cmdline = Mock(side_effect=psutil.NoSuchProcess(123))
with patch('psutil.Process.children', Mock(return_value=[c1, c2, c3])):
proc = PostmasterProcess(123) proc = PostmasterProcess(123)
self.assertIsNone(proc.wait_for_user_backends_to_close()) self.assertIsNone(proc.wait_for_user_backends_to_close())
mock_wait.assert_called_with([c2]) mock_wait.assert_called_with([c2])
c3.cmdline = Mock(side_effect=psutil.AccessDenied(123))
with patch('psutil.Process.children', Mock(return_value=[c3])):
proc = PostmasterProcess(123)
self.assertIsNone(proc.wait_for_user_backends_to_close())
@patch('subprocess.Popen') @patch('subprocess.Popen')
@patch.object(PostmasterProcess, 'from_pid') @patch.object(PostmasterProcess, 'from_pid')
def test_start(self, mock_frompid, mock_popen): def test_start(self, mock_frompid, mock_popen):