Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for CONFIG GET and CONFIG SET #49

Merged
merged 7 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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