Add session_redis

This commit is contained in:
Patrick Tombez
2019-05-10 17:32:49 +02:00
parent ab1f9595b1
commit 56ff5d6848
8 changed files with 257 additions and 0 deletions
+1
View File
@@ -1,4 +1,5 @@
boto==2.42.0
redis==2.10.5
python-json-logger==0.1.5
python-swiftclient==3.4.0
python-keystoneclient==3.13.0
+37
View File
@@ -0,0 +1,37 @@
Sessions in Redis
=================
This addon allows to store the web sessions in Redis.
Configuration
-------------
The storage of sessions in Redis is activated using environment variables.
* ``ODOO_SESSION_REDIS`` has to be ``1`` or ``true``
* ``ODOO_SESSION_REDIS_HOST`` is the redis hostname (default is ``localhost``)
* ``ODOO_SESSION_REDIS_PORT`` is the redis port (default is ``6379``)
* ``ODOO_SESSION_REDIS_PASSWORD`` is the password for the AUTH command
(optional)
* ``ODOO_SESSION_REDIS_PREFIX`` is the prefix for the session keys (optional)
* ``ODOO_SESSION_REDIS_EXPIRATION`` is the time in seconds before expiration of
the sessions (default is 7 days)
The keys are set to ``session:<session id>``.
When a prefix is defined, the keys are ``session:<prefix>:<session id>``
This addon must be added in the server wide addons with (``--load`` option):
``--load=web,web_kanban,session_redis``
Limitations
-----------
* The server has to be restarted in order for the sessions to be stored in
Redis.
* All the users will have to login again as their previous session will be
dropped.
* The addon monkey-patch ``openerp.http.Root.session_store`` with a custom
method when the Redis mode is active, so incompatibilities with other addons
is possible if they do the same.
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import http
from . import session
from . import models
+19
View File
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
{'name': 'Sessions in Redis',
'summary': 'Store web sessions in Redis',
'version': '7.0.1.0.0',
'author': 'Camptocamp,Odoo Community Association (OCA)',
'license': 'AGPL-3',
'category': 'Extra Tools',
'depends': ['base'],
'external_dependencies': {
'python': ['redis'],
},
'website': 'http://www.camptocamp.com',
'data': [],
'installable': True,
}
+92
View File
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import logging
import os
from distutils.util import strtobool
import openerp
from openerp.addons.web import http
from werkzeug.contrib.sessions import Session
from .session import RedisSessionStore
_logger = logging.getLogger(__name__)
try:
import redis
from redis.sentinel import Sentinel
except ImportError:
redis = None # noqa
_logger.debug("Cannot 'import redis'.")
def is_true(strval):
return bool(strtobool(strval or '0'.lower()))
sentinel_host = os.environ.get('ODOO_SESSION_REDIS_SENTINEL_HOST')
sentinel_master_name = os.environ.get(
'ODOO_SESSION_REDIS_SENTINEL_MASTER_NAME'
)
if sentinel_host and not sentinel_master_name:
raise Exception(
"ODOO_SESSION_REDIS_SENTINEL_MASTER_NAME must be defined "
"when using session_redis"
)
sentinel_port = int(os.environ.get('ODOO_SESSION_REDIS_SENTINEL_PORT', 26379))
host = os.environ.get('ODOO_SESSION_REDIS_HOST', 'localhost')
port = int(os.environ.get('ODOO_SESSION_REDIS_PORT', 6379))
prefix = os.environ.get('ODOO_SESSION_REDIS_PREFIX')
password = os.environ.get('ODOO_SESSION_REDIS_PASSWORD')
expiration = os.environ.get('ODOO_SESSION_REDIS_EXPIRATION')
def session_store():
if sentinel_host:
sentinel = Sentinel([(sentinel_host, sentinel_port)],
password=password)
redis_client = sentinel.master_for(sentinel_master_name)
else:
redis_client = redis.Redis(host=host, port=port, password=password)
return RedisSessionStore(redis=redis_client, prefix=prefix,
expiration=expiration,
session_class=Session)
def session_gc(session_store):
""" Do not garbage collect the sessions
Redis keys are automatically cleaned at the end of their
expiration.
"""
return
def purge_fs_sessions(path):
for fname in os.listdir(path):
path = os.path.join(path, fname)
try:
os.unlink(path)
except OSError:
pass
if is_true(os.environ.get('ODOO_SESSION_REDIS')):
if sentinel_host:
_logger.debug("HTTP sessions stored in Redis with prefix '%s'. "
"Using Sentinel on %s:%s",
sentinel_host, sentinel_port, prefix or '')
else:
_logger.debug("HTTP sessions stored in Redis with prefix '%s' on "
"%s:%s", host, port, prefix or '')
store = session_store()
for handler in openerp.service.wsgi_server.module_handlers:
if hasattr(handler, 'session_store'):
handler.session_store = store
http.session_gc = session_gc
# clean the existing sessions on the file system
purge_fs_sessions(http.session_path())
+1
View File
@@ -0,0 +1 @@
from . import user
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
from openerp.osv import osv
from openerp import tools
class User(osv.osv):
_inherit = 'res.users'
@tools.ormcache('sid')
def _compute_session_token(self, sid):
"""Make sure to return an unicode string.
Odoo creates a session token using hexdigest Session which is str
but with redis we set the token from a dictionary of values passing
it in json format. When dumping values from json, we always get unicode
thus both are incompatible.
The shortest path is to fix the output of the computed session by Odoo.
"""
return unicode(super(User, self)._compute_session_token(sid))
+78
View File
@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
from pickle import dumps, loads, HIGHEST_PROTOCOL
import logging
from werkzeug.contrib.sessions import SessionStore
# this is equal to the duration of the session garbage collector in
# openerp.http.session_gc()
DEFAULT_SESSION_TIMEOUT = 60 * 60 * 24 * 7 # 7 days in seconds
_logger = logging.getLogger(__name__)
class RedisSessionStore(SessionStore):
""" SessionStore that saves session to redis """
def __init__(self, redis, session_class=None,
prefix='', expiration=None):
super(RedisSessionStore, self).__init__(session_class=session_class)
self.redis = redis
if expiration is None:
self.expiration = DEFAULT_SESSION_TIMEOUT
else:
self.expiration = expiration
self.prefix = u'session:'
if prefix:
self.prefix = u'%s:%s:' % (
self.prefix, prefix
)
def build_key(self, sid):
if isinstance(sid, unicode):
sid = sid.encode('utf-8')
return '%s%s' % (self.prefix, sid)
def save(self, session):
key = self.build_key(session.sid)
if _logger.isEnabledFor(logging.DEBUG):
_logger.debug("saving session with key '%s' and "
"expiration of %s seconds",
key, self.expiration)
if self.redis.set(key, dumps(dict(session), HIGHEST_PROTOCOL)):
return self.redis.expire(key, self.expiration)
def delete(self, session):
key = self.build_key(session.sid)
_logger.debug('deleting session with key %s', key)
return self.redis.delete(key)
def get(self, sid):
if not self.is_valid_key(sid):
_logger.debug("session with invalid sid '%s' has been asked, "
"returning a new one", sid)
return self.new()
key = self.build_key(sid)
saved = self.redis.get(key)
if not saved:
_logger.debug("session with non-existent key '%s' has been asked, "
"returning a new one", key)
return self.new()
try:
data = loads(saved)
except ValueError:
_logger.debug("session for key '%s' has been asked but its json "
"content could not be read, it has been reset", key)
data = {}
return self.session_class(data, sid, False)
def list(self):
keys = self.redis.keys('%s*' % self.prefix)
_logger.debug("a listing redis keys has been called")
return [key[len(self.prefix):] for key in keys]