Skip to content

Commit

Permalink
Merge pull request #49 from Yipit/feature/config-get/set
Browse files Browse the repository at this point in the history
Add support for CONFIG GET and CONFIG SET
  • Loading branch information
hltbra authored Sep 4, 2019
2 parents a287ead + 45a1ea8 commit 0a498d3
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 38 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fulltests:
bash -c "trap 'make stop-testserver' EXIT; make start-testserver DEBUG=''; make test"

fulltests-real-redis:
bash -c "trap 'make stop-redistestserver' EXIT; make start-redistestserver; make test"
bash -c "trap 'make stop-redistestserver' EXIT; make start-redistestserver; make test REALREDIS=1"

test: unit integration lint

Expand Down
26 changes: 23 additions & 3 deletions dredis/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from functools import wraps

from dredis import config
from dredis.exceptions import AuthenticationRequiredError, CommandNotFound, DredisSyntaxError, DredisError
from dredis.utils import to_float

Expand Down Expand Up @@ -94,6 +95,25 @@ def cmd_save(keyspace):
return SimpleString('OK')


@command('CONFIG', arity=-2, flags=CMD_WRITE)
def cmd_config(keyspace, action, *params):
if action.lower() == 'help':
# copied from
# https:/antirez/redis/blob/cb51bb4320d2240001e8fc4a522d59fb28259703/src/config.c#L2244-L2245
help = [
"GET <pattern> -- Return parameters matching the glob-like <pattern> and their values.",
"SET <parameter> <value> -- Set parameter to value.",
]
return help
elif action.lower() == 'get' and len(params) == 1:
return config.get_all(params[0])
elif action.lower() == 'set' and len(params) == 2:
config.set(params[0], params[1])
return SimpleString('OK')
else:
raise DredisError("Unknown subcommand or wrong number of arguments for '{}'. Try CONFIG HELP.".format(action))


"""
****************
* Key commands *
Expand Down Expand Up @@ -469,17 +489,17 @@ def _validate_scan_params(args, cursor):
return cursor, count, match


def run_command(keyspace, cmd, args, readonly=False):
def run_command(keyspace, cmd, args):
logger.debug('[run_command] cmd={}, args={}'.format(repr(cmd), repr(args)))

str_args = map(str, args)
if cmd.upper() not in REDIS_COMMANDS:
raise CommandNotFound("unknown command '{}'".format(cmd))
else:
cmd_fn = REDIS_COMMANDS[cmd.upper()]
if keyspace.requirepass and not keyspace.authenticated and cmd_fn != cmd_auth:
if config.get('requirepass') != config.EMPTY and not keyspace.authenticated and cmd_fn != cmd_auth:
raise AuthenticationRequiredError()
if readonly and cmd_fn.flags & CMD_WRITE:
if config.get('readonly') == config.TRUE and cmd_fn.flags & CMD_WRITE:
raise DredisError("Can't execute %r in readonly mode" % cmd)
else:
return cmd_fn(keyspace, *str_args)
50 changes: 50 additions & 0 deletions dredis/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import fnmatch
import logging

from dredis.exceptions import DredisError
from dredis.utils import setup_logging

TRUE = 'true'
FALSE = 'false'
EMPTY = ''

_SERVER_CONFIG = {
'debug': FALSE,
'readonly': FALSE,
'requirepass': EMPTY,
}


def get_all(pattern):
result = []
for option, value in sorted(_SERVER_CONFIG.items()):
if not fnmatch.fnmatch(option, pattern):
continue
result.append(option)
result.append(value)
return result


def set(option, value):
if option in _SERVER_CONFIG:
if option == 'debug':
value = _validate_bool(option, value)
if value == TRUE:
setup_logging(logging.DEBUG)
else:
setup_logging(logging.INFO)
elif option == 'readonly':
value = _validate_bool(option, value)
_SERVER_CONFIG[option] = value
else:
raise DredisError('Unsupported CONFIG parameter: {}'.format(option))


def get(option):
return _SERVER_CONFIG[option]


def _validate_bool(option, value):
if value.lower() not in (TRUE, FALSE):
raise DredisError("Invalid argument '{}' for CONFIG SET '{}'".format(value, option))
return value.lower()
10 changes: 4 additions & 6 deletions dredis/keyspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import time
from io import BytesIO

from dredis import rdb
from dredis import rdb, config
from dredis.db import DB_MANAGER, KEY_CODEC, DEFAULT_REDIS_DB
from dredis.exceptions import DredisError, BusyKeyError, NoKeyError
from dredis.lua import LuaRunner
Expand Down Expand Up @@ -56,12 +56,10 @@ def _get_cursor_key(self, db, key, cursor_id):

class Keyspace(object):

def __init__(self, password=None):
def __init__(self):
self._lua_runner = LuaRunner(self)
self._current_db = DEFAULT_REDIS_DB
self._set_db(self._current_db)
self._password = password
self.requirepass = password is not None
self.authenticated = False

def _set_db(self, db):
Expand Down Expand Up @@ -547,9 +545,9 @@ def rename(self, old_name, new_name):
raise NoKeyError()

def auth(self, password):
if not self.requirepass:
if config.get('requirepass') == config.EMPTY:
raise DredisError("client sent AUTH, but no password is set")
if self._password != password:
if password != config.get('requirepass'):
self.authenticated = False
raise DredisError("invalid password")
else:
Expand Down
30 changes: 8 additions & 22 deletions dredis/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import sys

from dredis import __version__
from dredis import db, rdb
from dredis import db, rdb, config
from dredis.commands import run_command, SimpleString
from dredis.exceptions import DredisError
from dredis.keyspace import Keyspace, to_float_string
Expand All @@ -22,13 +22,11 @@
logger = logging.getLogger('dredis')

ROOT_DIR = None # defined by `main()`
READONLY_SERVER = False
REQUIREPASS = None


def execute_cmd(keyspace, send_fn, cmd, *args):
try:
result = run_command(keyspace, cmd, args, readonly=READONLY_SERVER)
result = run_command(keyspace, cmd, args)
except DredisError as exc:
transmit(send_fn, exc)
except Exception as exc:
Expand Down Expand Up @@ -79,7 +77,7 @@ class CommandHandler(asyncore.dispatcher):
def __init__(self, *args, **kwargs):
asyncore.dispatcher.__init__(self, *args, **kwargs)
self._parser = Parser(self.recv) # contains client message buffer
self.keyspace = Keyspace(password=REQUIREPASS)
self.keyspace = Keyspace()

def handle_read(self):
try:
Expand Down Expand Up @@ -124,14 +122,6 @@ def handle_accept(self):
CommandHandler(sock)


def setup_logging(level):
logger.setLevel(level)
formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
logger.addHandler(handler)


def main():
parser = argparse.ArgumentParser(version=__version__)
parser.add_argument('--host', default='127.0.0.1', help='server host (defaults to %(default)s)')
Expand All @@ -147,7 +137,7 @@ def main():
parser.add_argument('--debug', action='store_true', help='enable debug logs')
parser.add_argument('--flushall', action='store_true', default=False, help='run FLUSHALL on startup')
parser.add_argument('--readonly', action='store_true', help='accept read-only commands')
parser.add_argument('--requirepass', default=None,
parser.add_argument('--requirepass', default='',
help='require clients to issue AUTH <password> before processing any other commands')
args = parser.parse_args()

Expand All @@ -160,17 +150,13 @@ def main():
ROOT_DIR = tempfile.mkdtemp(prefix="redis-test-")

if args.debug:
setup_logging(logging.DEBUG)
else:
setup_logging(logging.INFO)
config.set('debug', 'true')

if args.readonly:
global READONLY_SERVER
READONLY_SERVER = True
config.set('readonly', 'true')

if args.requirepass:
global REQUIREPASS
REQUIREPASS = args.requirepass
config.set('requirepass', args.requirepass)

db_backend_options = {}
if args.backend_option:
Expand Down Expand Up @@ -199,7 +185,7 @@ def main():
logger.info("Port: {}".format(args.port))
logger.info("Root directory: {}".format(ROOT_DIR))
logger.info('PID: {}'.format(os.getpid()))
logger.info('Readonly: {}'.format(READONLY_SERVER))
logger.info('Readonly: {}'.format(config.get('readonly')))
logger.info('Ready to accept connections')

try:
Expand Down
11 changes: 11 additions & 0 deletions dredis/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import logging
import struct
import sys


def to_float(s):
Expand Down Expand Up @@ -54,3 +56,12 @@ def _flip_bits(self, bytestring):


FLOAT_CODEC = FloatCodec()


def setup_logging(level):
logger = logging.getLogger('dredis')
logger.setLevel(level)
formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
logger.addHandler(handler)
60 changes: 60 additions & 0 deletions tests/integration/test_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import glob
import os.path

import pytest
import redis

from tests.helpers import fresh_redis


Expand Down Expand Up @@ -57,3 +60,60 @@ def test_save_creates_an_rdb_file():
r.set('test', 'value')
assert r.save()
assert len(set(glob.glob(os.path.join(root_dir, 'dump*.rdb'))) - rdb_files_before) == 1


def test_config_help():
r = fresh_redis()
result = r.execute_command('CONFIG', 'HELP')

# assert strings individually because redis has more help lines than dredis
assert "GET <pattern> -- Return parameters matching the glob-like <pattern> and their values." in result
assert "SET <parameter> <value> -- Set parameter to value." in result


def test_config_get_with_unknown_config():
r = fresh_redis()

assert r.config_get('foo') == {}


def test_config_get_with_wrong_number_of_arguments():
r = fresh_redis()

with pytest.raises(redis.ResponseError) as exc:
r.execute_command('CONFIG', 'GET', 'foo', 'bar', 'baz')

assert str(exc.value) == "Unknown subcommand or wrong number of arguments for 'GET'. Try CONFIG HELP."


def test_config_set_with_unknown_config():
r = fresh_redis()

with pytest.raises(redis.ResponseError) as exc:
r.config_set('foo', 'bar')

assert str(exc.value) == "Unsupported CONFIG parameter: foo"


@pytest.mark.skipif(os.getenv('REALREDIS') == '1', reason="these options only exist in dredis")
def test_config_get():
r = fresh_redis()

assert sorted(r.config_get('*').keys()) == sorted(['debug', 'readonly', 'requirepass'])
assert r.config_get('*deb*').keys() == ['debug']


@pytest.mark.skipif(os.getenv('REALREDIS') == '1', reason="these options only exist in dredis")
def test_config_set():
r = fresh_redis()
original_value = r.config_get('debug')['debug']

try:
assert r.config_set('debug', 'false')
assert r.config_get('debug') == {'debug': 'false'}

assert r.config_set('debug', 'true')
assert r.config_get('debug') == {'debug': 'true'}
finally:
# undo it to not affect other tests
assert r.config_set('debug', original_value)
6 changes: 5 additions & 1 deletion tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import pytest

from dredis import config
from dredis.db import DB_MANAGER
from dredis.keyspace import Keyspace


@pytest.fixture
def keyspace():
DB_MANAGER.setup_dbs('', backend='memory', backend_options={})
return Keyspace()
original_configs = config.get_all('*')
yield Keyspace()
for option, value in zip(original_configs[0::2], original_configs[1::2]):
config.set(option, value)
14 changes: 9 additions & 5 deletions tests/unit/test_requirepass.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
import pytest

from dredis import config
from dredis.commands import run_command
from dredis.exceptions import AuthenticationRequiredError, DredisError
from dredis.keyspace import Keyspace


def test_raises_error_if_not_authenticated(keyspace):
config.set('requirepass', 'test')
with pytest.raises(AuthenticationRequiredError) as exc:
run_command(Keyspace(password='test'), 'get', ('test',))
run_command(Keyspace(), 'get', ('test',))

assert str(exc.value) == 'NOAUTH Authentication required.'


def test_raises_error_if_password_is_wrong(keyspace):
k = Keyspace(password='secret')

config.set('requirepass', 'test')
k = Keyspace()
with pytest.raises(DredisError) as exc:
k.auth('wrongpass')

assert str(exc.value) == 'ERR invalid password'


def test_allows_commands_when_password_is_valid(keyspace):
k = Keyspace(password='secret')
config.set('requirepass', 'secret')
k = Keyspace()

assert run_command(k, 'auth', ('secret',))
assert run_command(k, 'incrby', ('counter', '1')) == 1


def test_bad_authentication_when_authenticated_should_invalidate_the_session(keyspace):
k = Keyspace(password='secret')
config.set('requirepass', 'secret')
k = Keyspace()

assert run_command(k, 'auth', ('secret',))
try:
Expand Down

0 comments on commit 0a498d3

Please sign in to comment.