Compare commits

...
Author SHA1 Message Date
Alexander Kukushkin b3f109751c Merge branch 'master' of github.com:zalando/patroni into feature/terminaltables 2020-06-30 16:56:47 +02:00
Alexander Kukushkin 52761ac46c Merge branch 'master' of github.com:zalando/patroni into feature/terminaltables 2020-04-15 12:29:12 +02:00
Alexander Kukushkin 7c409f59d7 Switch to texttable
it seems to be well maintained and packages are available even for old distros.
2020-02-19 12:29:25 +01:00
Alexander Kukushkin ee79a390c2 Fix little bug and unit-tests 2020-02-14 14:30:48 +01:00
Alexander Kukushkin 6dcaa697c0 Replace prettytable with terminaltables
It is a more advanced library and can deal with multi-line strings, what
allows us to present tags in a nice format. It also could nicely show
the table header, so we get rid of redundant Cluster column.

Debian/Ubuntu already have the module packaged as `python3-terminaltables`,
so it shouldn't be a problem for package maintainers.

Example output:
```bash
$ patronictl list
┌ Cluster: batman (6792870884189948744) ┬─────────┬────┬───────────┬─────────────────┬────────────────────────────┐
│ Member      │ Host           │ Role   │ State   │ TL │ Lag in MB │ Pending restart │ Tags                       │
├─────────────┼────────────────┼────────┼─────────┼────┼───────────┼─────────────────┼────────────────────────────┤
│ postgresql0 │ 127.0.0.1:5432 │ Leader │ running │  2 │           │                 │                            │
├─────────────┼────────────────┼────────┼─────────┼────┼───────────┼─────────────────┼────────────────────────────┤
│ postgresql1 │ 127.0.0.1:5433 │        │ running │  2 │         0 │                 │ clonefrom: true            │
│             │                │        │         │    │           │                 │ nofailover: true           │
│             │                │        │         │    │           │                 │ noloadbalance: true        │
│             │                │        │         │    │           │                 │ replicatefrom: postgresql0 │
├─────────────┼────────────────┼────────┼─────────┼────┼───────────┼─────────────────┼────────────────────────────┤
│ postgresql2 │ 127.0.0.1:5434 │        │ running │  2 │         0 │ *               │ replicatefrom: postgres1   │
└─────────────┴────────────────┴────────┴─────────┴────┴───────────┴─────────────────┴────────────────────────────┘

$ patronictl list badclustername
┌ Cluster: badclustername (uninitialized) ──────┐
│ Member │ Host │ Role │ State │ TL │ Lag in MB │
└────────┴──────┴──────┴───────┴────┴───────────┘

$ patronictl history
┌────┬──────────┬──────────────────────────────┬───────────────────────────┐
│ TL │      LSN │ Reason                       │ Timestamp                 │
├────┼──────────┼──────────────────────────────┼───────────────────────────┤
│  1 │ 25657792 │ no recovery target specified │ 2020-02-13T11:26:27+01:00 │
│  2 │ 25690088 │ no recovery target specified │ 2020-02-13T11:29:53+01:00 │
└────┴──────────┴──────────────────────────────┴───────────────────────────┘

$ patronictl query -c "SELECT a, repeat('x', a) from generate_series(1,3) a"
┌───┬────────┐
│ a │ repeat │
├───┼────────┤
│ 1 │ x      │
│ 2 │ xx     │
│ 3 │ xxx    │
└───┴────────┘
```
2020-02-14 12:38:35 +01:00
3 changed files with 47 additions and 26 deletions
+37 -22
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
''' '''
Patroni Control Patroni Control
''' '''
@@ -32,8 +33,8 @@ from patroni.postgresql.misc import postgres_version_to_int
from patroni.utils import cluster_as_json, patch_config, polling_loop from patroni.utils import cluster_as_json, patch_config, polling_loop
from patroni.request import PatroniRequest from patroni.request import PatroniRequest
from patroni.version import __version__ from patroni.version import __version__
from prettytable import ALL, FRAME, PrettyTable
from six.moves.urllib_parse import urlparse from six.moves.urllib_parse import urlparse
from texttable import Texttable
CONFIG_DIR_PATH = click.get_app_dir('patroni') 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')
@@ -47,32 +48,42 @@ class PatroniCtlException(ClickException):
pass pass
class PatronictlPrettyTable(PrettyTable): class PatronictlPrettyTable(Texttable):
def __init__(self, header, *args, **kwargs): def __init__(self, header=None):
PrettyTable.__init__(self, *args, **kwargs) Texttable.__init__(self, 0)
self.__table_header = header self.__table_header = header
self.__hline_num = 0 self.__hline_num = 0
self.__hline = None
if sys.platform != 'win32':
self._char_horiz = u''
self._char_vert = u''
self._hline_header = self._hline
self._char_header = self._char_horiz
def _is_first_hline(self): def _is_first_hline(self):
return self.__hline_num == 0 return self.__hline_num == 0
def _set_hline(self, value): def _is_last_hline(self):
self.__hline = value return self.__hline_num > (len(self._rows) if self._has_hlines() else int(bool(self._header)))
def _get_hline(self): def _hline(self):
ret = self.__hline if sys.platform == 'win32':
left = right = self._char_corner
elif self._is_first_hline():
left, self._char_corner, right = u'', u'', u''
elif not self._is_last_hline():
left, self._char_corner, right = u'', u'', u''
else:
left, self._char_corner, right = u'', u'', u''
line = self._build_hline()
# Inject nice table header
if self._is_first_hline() and self.__table_header: if self._is_first_hline() and self.__table_header:
header = self.__table_header[:len(ret) - 2] left += self.__table_header
ret = "".join([ret[0], header, ret[1 + len(header):]])
self.__hline_num += 1 self.__hline_num += 1
return ret return left + line[len(left):-2] + right + '\n'
_hrule = property(_get_hline, _set_hline)
def parse_dcs(dcs): def parse_dcs(dcs):
@@ -186,13 +197,17 @@ def print_output(columns, rows, alignment=None, fmt='pretty', header=None, delim
for r in ([columns] if columns else []) + rows: for r in ([columns] if columns else []) + rows:
click.echo(delimiter.join(map(str, r))) click.echo(delimiter.join(map(str, r)))
else: else:
hrules = ALL if any(any(isinstance(c, six.string_types) and '\n' in c for c in r) for r in rows) else FRAME table = PatronictlPrettyTable(header)
table = PatronictlPrettyTable(header, columns, hrules=hrules) if not any(any(isinstance(c, six.string_types) and '\n' in c for c in r) for r in rows):
for k, v in (alignment or {}).items(): table.set_deco(Texttable.VLINES | Texttable.BORDER | Texttable.HEADER)
table.align[k] = v if rows:
for r in rows: if columns:
table.add_row(r) table.header(columns)
click.echo(table) table.set_cols_align([(alignment or {}).get(c, 'l') for c in columns])
table.add_rows(rows, header=False)
else:
table.add_rows([columns], header=False)
click.echo(table.draw())
def watching(w, watch, max_count=None, clear=True): def watching(w, watch, max_count=None, clear=True):
+1 -1
View File
@@ -6,7 +6,7 @@ kazoo>=1.3.1
python-etcd>=0.4.3,<0.5 python-etcd>=0.4.3,<0.5
python-consul>=0.7.1 python-consul>=0.7.1
click>=4.1 click>=4.1
prettytable>=0.7 texttable
python-dateutil python-dateutil
psutil>=2.0.0 psutil>=2.0.0
cdiff cdiff
+9 -3
View File
@@ -5,9 +5,9 @@ import unittest
from click.testing import CliRunner from click.testing import CliRunner
from datetime import datetime, timedelta from datetime import datetime, timedelta
from mock import patch, Mock from mock import patch, Mock
from patroni.ctl import ctl, store_config, load_config, output_members, get_dcs, parse_dcs, \ from patroni.ctl import ctl, store_config, load_config, output_members, get_dcs, parse_dcs, get_all_members, \
get_all_members, get_any_member, get_cursor, query_member, configure, PatroniCtlException, apply_config_changes, \ get_any_member, get_cursor, query_member, configure, PatroniCtlException, apply_config_changes, show_diff, \
format_config_for_editing, show_diff, invoke_editor, format_pg_version, find_executable, CONFIG_FILE_PATH format_config_for_editing, invoke_editor, format_pg_version, find_executable, print_output, CONFIG_FILE_PATH
from patroni.dcs.etcd import Client, Failover from patroni.dcs.etcd import Client, Failover
from patroni.utils import tzutc from patroni.utils import tzutc
from psycopg2 import OperationalError from psycopg2 import OperationalError
@@ -165,6 +165,12 @@ class TestCtl(unittest.TestCase):
def test_get_dcs(self): def test_get_dcs(self):
self.assertRaises(PatroniCtlException, get_dcs, {'dummy': {}}, 'dummy') self.assertRaises(PatroniCtlException, get_dcs, {'dummy': {}}, 'dummy')
@patch('sys.platform', 'win32')
@patch('click.echo')
def test_print_output(self, mock_click_echo):
print_output(['a'], [])
mock_click_echo.assert_called_once_with('+---+\n| a |\n+---+')
@patch('psycopg2.connect', psycopg2_connect) @patch('psycopg2.connect', psycopg2_connect)
@patch('patroni.ctl.query_member', Mock(return_value=([['mock column']], None))) @patch('patroni.ctl.query_member', Mock(return_value=([['mock column']], None)))
@patch('patroni.ctl.get_dcs') @patch('patroni.ctl.get_dcs')