Repository: redis/redis-py

Issue:
Adding Sentinel Commands
Sentinel API [commands](https://redis.io/topics/sentinel)


Discussion:
@andymccurdy Could you please review this PR?
There are already sentinel commands in `redis.client`, I think these should be added there:

https://github.com/andymccurdy/redis-py/blob/master/redis/client.py#L792
What do you think @andymccurdy ? Should I take @lalinsky suggestion?
This pull request is marked stale. It will be closed in 30 days if it is not updated.
This pull request is marked stale. It will be closed in 30 days if it is not updated.
@otherpirate Thanks for the contribution. This is great! I'd like to get it in right after [this pr](https://github.com/andymccurdy/redis-py/pull/1534) if that's alright with you.

Alternatively if this is no longer active, I'll progress with it. Please let me know!

Resolving patch:
diff --git a/redis/client.py b/redis/client.py
--- a/redis/client.py
+++ b/redis/client.py
@@ -675,10 +675,14 @@ class Redis(Commands, object):
         'SCRIPT FLUSH': bool_ok,
         'SCRIPT KILL': bool_ok,
         'SCRIPT LOAD': str_if_bytes,
+        'SENTINEL CKQUORUM': bool_ok,
+        'SENTINEL FAILOVER': bool_ok,
+        'SENTINEL FLUSHCONFIG': bool_ok,
         'SENTINEL GET-MASTER-ADDR-BY-NAME': parse_sentinel_get_master,
         'SENTINEL MASTER': parse_sentinel_master,
         'SENTINEL MASTERS': parse_sentinel_masters,
         'SENTINEL MONITOR': bool_ok,
+        'SENTINEL RESET': bool_ok,
         'SENTINEL REMOVE': bool_ok,
         'SENTINEL SENTINELS': parse_sentinel_slaves_and_sentinels,
         'SENTINEL SET': bool_ok,
diff --git a/redis/commands.py b/redis/commands.py
--- a/redis/commands.py
+++ b/redis/commands.py
@@ -2950,7 +2950,7 @@ def execute(self):
         return self.client.execute_command(*command)
 
 
-class SentinalCommands:
+class SentinelCommands:
     """
     A class containing the commands specific to redis sentinal. This class is
     to be used as a mixin.
@@ -2993,3 +2993,54 @@ def sentinel_set(self, name, option, value):
     def sentinel_slaves(self, service_name):
         "Returns a list of slaves for ``service_name``"
         return self.execute_command('SENTINEL SLAVES', service_name)
+
+    def sentinel_reset(self, pattern):
+        """
+        This command will reset all the masters with matching name.
+        The pattern argument is a glob-style pattern.
+
+        The reset process clears any previous state in a master (including a
+        failover in progress), and removes every slave and sentinel already
+        discovered and associated with the master.
+        """
+        return self.execute_command('SENTINEL RESET', pattern, once=True)
+
+    def sentinel_failover(self, new_master_name):
+        """
+        Force a failover as if the master was not reachable, and without
+        asking for agreement to other Sentinels (however a new version of the
+        configuration will be published so that the other Sentinels will
+        update their configurations).
+        """
+        return self.execute_command('SENTINEL FAILOVER', new_master_name)
+
+    def sentinel_ckquorum(self, new_master_name):
+        """
+        Check if the current Sentinel configuration is able to reach the
+        quorum needed to failover a master, and the majority needed to
+        authorize the failover.
+
+        This command should be used in monitoring systems to check if a
+        Sentinel deployment is ok.
+        """
+        return self.execute_command('SENTINEL CKQUORUM',
+                                    new_master_name,
+                                    once=True)
+
+    def sentinel_flushconfig(self):
+        """
+        Force Sentinel to rewrite its configuration on disk, including the
+        current Sentinel state.
+
+        Normally Sentinel rewrites the configuration every time something
+        changes in its state (in the context of the subset of the state which
+        is persisted on disk across restart).
+        However sometimes it is possible that the configuration file is lost
+        because of operation errors, disk failures, package upgrade scripts or
+        configuration managers. In those cases a way to to force Sentinel to
+        rewrite the configuration file is handy.
+
+        This command works even if the previous configuration file is
+        completely missing.
+        """
+        return self.execute_command('SENTINEL FLUSHCONFIG')
diff --git a/redis/sentinel.py b/redis/sentinel.py
--- a/redis/sentinel.py
+++ b/redis/sentinel.py
@@ -2,7 +2,7 @@
 import weakref
 
 from redis.client import Redis
-from redis.commands import SentinalCommands
+from redis.commands import SentinelCommands
 from redis.connection import ConnectionPool, Connection
 from redis.exceptions import (ConnectionError, ResponseError, ReadOnlyError,
                               TimeoutError)
@@ -133,7 +133,7 @@ def rotate_slaves(self):
         raise SlaveNotFoundError('No slave found for %r' % (self.service_name))
 
 
-class Sentinel(SentinalCommands, object):
+class Sentinel(SentinelCommands, object):
     """
     Redis Sentinel cluster client
 
@@ -179,6 +179,23 @@ def __init__(self, sentinels, min_other_sentinels=0, sentinel_kwargs=None,
         self.min_other_sentinels = min_other_sentinels
         self.connection_kwargs = connection_kwargs
 
+    def execute_command(self, *args, **kwargs):
+        """
+        Execute Sentinel command in sentinel nodes.
+        once - If set to True, then execute the resulting command on a single
+               node at random, rather than across the entire sentinel cluster.
+        """
+        once = bool(kwargs.get('once', False))
+        if 'once' in kwargs.keys():
+            kwargs.pop('once')
+
+        if once:
+            for sentinel in self.sentinels:
+                sentinel.execute_command(*args, **kwargs)
+        else:
+            random.choice(self.sentinels).execute_command(*args, **kwargs)
+        return True
+
     def __repr__(self):
         sentinel_addresses = []
         for sentinel in self.sentinels:


Test patch:
diff --git a/tests/test_sentinel.py b/tests/test_sentinel.py
--- a/tests/test_sentinel.py
+++ b/tests/test_sentinel.py
@@ -30,9 +30,15 @@ def sentinel_slaves(self, master_name):
             return []
         return self.cluster.slaves
 
+    def execute_command(self, *args, **kwargs):
+        # wrapper  purely to validate the calls don't explode
+        from redis.client import bool_ok
+        return bool_ok
+
 
 class SentinelTestCluster:
-    def __init__(self, service_name='mymaster', ip='127.0.0.1', port=6379):
+    def __init__(self, servisentinel_ce_name='mymaster', ip='127.0.0.1',
+                 port=6379):
         self.clients = {}
         self.master = {
             'ip': ip,
@@ -42,7 +48,7 @@ def __init__(self, service_name='mymaster', ip='127.0.0.1', port=6379):
             'is_odown': False,
             'num-other-sentinels': 0,
         }
-        self.service_name = service_name
+        self.service_name = servisentinel_ce_name
         self.slaves = []
         self.nodes_down = set()
         self.nodes_timeout = set()
@@ -198,3 +204,16 @@ def test_slave_round_robin(cluster, sentinel, master_ip):
     assert next(rotator) == (master_ip, 6379)
     with pytest.raises(SlaveNotFoundError):
         next(rotator)
+
+
+def test_ckquorum(cluster, sentinel):
+    assert sentinel.sentinel_ckquorum("mymaster")
+
+
+def test_flushconfig(cluster, sentinel):
+    assert sentinel.sentinel_flushconfig()
+
+
+def test_reset(cluster, sentinel):
+    cluster.master['is_odown'] = True
+    assert sentinel.sentinel_reset('mymaster')