Skip to content

Commit 4348b44

Browse files
committed
Refactor session db logging and key generation for sessions/otks
While I was working on the redis sessiondb stuff, I noticed that log_wanrning, get_logger ... was duplicated. Also there was code to generate a unique key for otks that was duplicated. Changes: creating new sessions_common.py and SessionsCommon class to provide methods: log_warning, log_info, log_debug, get_logger, getUniqueKey getUniqueKey method is closer to the method used to make session keys in client.py. sessions_common.py now report when random_.py chooses a weak random number generator. Removed same from rest.py. get_logger reconciles all logging under roundup.hyperdb.backends.<name of BasicDatabase class> some backends used to log to root logger. have BasicDatabase in other sessions_*.py modules inherit from SessionCommon. change logging to use log_* methods. In addition: remove unused imports reported by flake8 and other formatting changes modify actions.py, rest.py, templating.py to use getUniqueKey method. add tests for new methods test_redis_session.py swap out ModuleNotFoundError for ImportError to prevent crash in python2 when redis is not present. allow injection of username:password or just password into redis connection URL. set pytest_redis_pw envirnment variable to password or user:password when running test.
1 parent 426a0eb commit 4348b44

File tree

10 files changed

+216
-103
lines changed

10 files changed

+216
-103
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import base64, logging
2+
3+
import roundup.anypy.random_ as random_
4+
from roundup.anypy.strings import b2s
5+
6+
logger = logging.getLogger('roundup.hyperdb.backend.sessions')
7+
if not random_.is_weak:
8+
logger.debug("Importing good random generator")
9+
else:
10+
logger.warning("**SystemRandom not available. Using poor random generator")
11+
12+
13+
class SessionCommon:
14+
15+
def log_debug(self, msg, *args, **kwargs):
16+
"""Log a message with level DEBUG."""
17+
18+
logger = self.get_logger()
19+
logger.debug(msg, *args, **kwargs)
20+
21+
def log_info(self, msg, *args, **kwargs):
22+
"""Log a message with level INFO."""
23+
24+
logger = self.get_logger()
25+
logger.info(msg, *args, **kwargs)
26+
27+
def log_warning(self, msg, *args, **kwargs):
28+
"""Log a message with level INFO."""
29+
logger = self.get_logger()
30+
logger.warning(msg, *args, **kwargs)
31+
32+
def get_logger(self):
33+
"""Return the logger for this database."""
34+
35+
# Because getting a logger requires acquiring a lock, we want
36+
# to do it only once.
37+
if not hasattr(self, '__logger'):
38+
self.__logger = logging.getLogger('roundup.hyperdb.backends.%s' %
39+
self.name or "basicdb" )
40+
41+
return self.__logger
42+
43+
def getUniqueKey(self, length=40):
44+
otk = b2s(base64.b64encode(
45+
random_.token_bytes(length))).rstrip('=')
46+
while self.exists(otk):
47+
otk = b2s(base64.b64encode(
48+
random_.token_bytes(length))).rstrip('=')
49+
50+
return otk

roundup/backends/sessions_dbm.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@
66
"""
77
__docformat__ = 'restructuredtext'
88

9-
import os, marshal, time, logging, random
9+
import marshal, os, random, time
1010

1111
from roundup.anypy.html import html_escape as escape
1212

1313
from roundup import hyperdb
1414
from roundup.i18n import _
1515
from roundup.anypy.dbm_ import anydbm, whichdb
16+
from roundup.backends.sessions_common import SessionCommon
1617

1718

18-
class BasicDatabase:
19+
class BasicDatabase(SessionCommon):
1920
''' Provide a nice encapsulation of an anydbm store.
2021
2122
Keys are id strings, values are automatically marshalled data.
@@ -88,7 +89,7 @@ def getall(self, infoid):
8889

8990
def set(self, infoid, **newvalues):
9091
db = self.opendb('c')
91-
timestamp=None
92+
timestamp = None
9293
try:
9394
if infoid in db:
9495
values = marshal.loads(db[infoid])
@@ -147,7 +148,6 @@ def opendb(self, mode):
147148
dbm = __import__(db_type)
148149

149150
retries_left = 15
150-
logger = logging.getLogger('roundup.hyperdb.backend.sessions')
151151
while True:
152152
try:
153153
handle = dbm.open(path, mode)
@@ -157,14 +157,16 @@ def opendb(self, mode):
157157
# [Errno 11] Resource temporarily unavailable retry
158158
# FIXME: make this more specific
159159
if retries_left < 10:
160-
logger.warning('dbm.open failed on ...%s, retry %s left: %s, %s'%(path[-15:],15-retries_left,retries_left,e))
160+
self.log_warning(
161+
'dbm.open failed on ...%s, retry %s left: %s, %s' %
162+
(path[-15:], 15-retries_left, retries_left, e))
161163
if retries_left < 0:
162164
# We have used up the retries. Reraise the exception
163165
# that got us here.
164166
raise
165167
else:
166168
# stagger retry to try to get around thundering herd issue.
167-
time.sleep(random.randint(0,25)*.005)
169+
time.sleep(random.randint(0, 25)*.005)
168170
retries_left = retries_left - 1
169171
continue # the while loop
170172
return handle

roundup/backends/sessions_rdbms.py

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,67 +5,70 @@
55
class. It's now also used for One Time Key handling too.
66
"""
77
__docformat__ = 'restructuredtext'
8-
import os, time, logging
8+
import time
99

1010
from roundup.anypy.html import html_escape as escape
11+
from roundup.backends.sessions_common import SessionCommon
1112

12-
class BasicDatabase:
13+
14+
class BasicDatabase(SessionCommon):
1315
''' Provide a nice encapsulation of an RDBMS table.
1416
1517
Keys are id strings, values are automatically marshalled data.
1618
'''
1719
name = None
20+
1821
def __init__(self, db):
1922
self.db = db
2023
self.conn, self.cursor = self.db.sql_open_connection()
2124

2225
def clear(self):
23-
self.cursor.execute('delete from %ss'%self.name)
26+
self.cursor.execute('delete from %ss' % self.name)
2427

2528
def exists(self, infoid):
2629
n = self.name
27-
self.cursor.execute('select count(*) from %ss where %s_key=%s'%(n,
28-
n, self.db.arg), (infoid,))
30+
self.cursor.execute('select count(*) from %ss where %s_key=%s' %
31+
(n, n, self.db.arg), (infoid,))
2932
return int(self.cursor.fetchone()[0])
3033

3134
_marker = []
35+
3236
def get(self, infoid, value, default=_marker):
3337
n = self.name
34-
self.cursor.execute('select %s_value from %ss where %s_key=%s'%(n,
35-
n, n, self.db.arg), (infoid,))
38+
self.cursor.execute('select %s_value from %ss where %s_key=%s' %
39+
(n, n, n, self.db.arg), (infoid,))
3640
res = self.cursor.fetchone()
3741
if not res:
3842
if default != self._marker:
3943
return default
40-
raise KeyError('No such %s "%s"'%(self.name, escape(infoid)))
44+
raise KeyError('No such %s "%s"' % (self.name, escape(infoid)))
4145
values = eval(res[0])
4246
return values.get(value, None)
4347

4448
def getall(self, infoid):
4549
n = self.name
46-
self.cursor.execute('select %s_value from %ss where %s_key=%s'%(n,
47-
n, n, self.db.arg), (infoid,))
50+
self.cursor.execute('select %s_value from %ss where %s_key=%s' %
51+
(n, n, n, self.db.arg), (infoid,))
4852
res = self.cursor.fetchone()
4953
if not res:
50-
raise KeyError('No such %s "%s"'%(self.name, escape (infoid)))
54+
raise KeyError('No such %s "%s"' % (self.name, escape(infoid)))
5155
return eval(res[0])
5256

5357
def set(self, infoid, **newvalues):
5458
""" Store all newvalues under key infoid with a timestamp in database.
5559
56-
If newvalues['__timestamp'] exists and is representable as a floating point number
57-
(i.e. could be generated by time.time()), that value is used for the <name>_time
58-
column in the database.
60+
If newvalues['__timestamp'] exists and is representable as
61+
a floating point number (i.e. could be generated by time.time()),
62+
that value is used for the <name>_time column in the database.
5963
"""
6064
c = self.cursor
6165
n = self.name
6266
a = self.db.arg
63-
c.execute('select %s_value from %ss where %s_key=%s'% \
64-
(n, n, n, a),
65-
(infoid,))
67+
c.execute('select %s_value from %ss where %s_key=%s' %
68+
(n, n, n, a), (infoid,))
6669
res = c.fetchone()
6770

68-
timestamp=time.time()
71+
timestamp = time.time()
6972
if res:
7073
values = eval(res[0])
7174
else:
@@ -85,43 +88,43 @@ def set(self, infoid, **newvalues):
8588
values.update(newvalues)
8689
if res:
8790
sql = ('update %ss set %s_value=%s, %s_time=%s '
88-
'where %s_key=%s'%(n, n, a, n, a, n, a))
91+
'where %s_key=%s' % (n, n, a, n, a, n, a))
8992
args = (repr(values), timestamp, infoid)
9093
else:
9194
sql = 'insert into %ss (%s_key, %s_time, %s_value) '\
92-
'values (%s, %s, %s)'%(n, n, n, n, a, a, a)
95+
'values (%s, %s, %s)' % (n, n, n, n, a, a, a)
9396
args = (infoid, timestamp, repr(values))
9497
c.execute(sql, args)
9598

9699
def list(self):
97100
c = self.cursor
98101
n = self.name
99-
c.execute('select %s_key from %ss'%(n, n))
102+
c.execute('select %s_key from %ss' % (n, n))
100103
return [res[0] for res in c.fetchall()]
101104

102105
def destroy(self, infoid):
103-
self.cursor.execute('delete from %ss where %s_key=%s'%(self.name,
104-
self.name, self.db.arg), (infoid,))
106+
self.cursor.execute('delete from %ss where %s_key=%s' %
107+
(self.name, self.name, self.db.arg), (infoid,))
105108

106109
def updateTimestamp(self, infoid):
107110
""" don't update every hit - once a minute should be OK """
108111
now = time.time()
109-
self.cursor.execute('''update %ss set %s_time=%s where %s_key=%s
110-
and %s_time < %s'''%(self.name, self.name, self.db.arg,
111-
self.name, self.db.arg, self.name, self.db.arg),
112-
(now, infoid, now-60))
112+
self.cursor.execute('''update %ss set %s_time=%s where %s_key=%s '''
113+
'''and %s_time < %s''' %
114+
(self.name, self.name, self.db.arg, self.name,
115+
self.db.arg, self.name, self.db.arg),
116+
(now, infoid, now-60))
113117

114118
def clean(self):
115119
''' Remove session records that haven't been used for a week. '''
116120
now = time.time()
117121
week = 60*60*24*7
118122
old = now - week
119-
self.cursor.execute('delete from %ss where %s_time < %s'%(self.name,
120-
self.name, self.db.arg), (old, ))
123+
self.cursor.execute('delete from %ss where %s_time < %s' %
124+
(self.name, self.name, self.db.arg), (old, ))
121125

122126
def commit(self):
123-
logger = logging.getLogger('roundup.hyperdb.backend')
124-
logger.info('commit %s' % self.name)
127+
self.log_info('commit %s' % self.name)
125128
self.conn.commit()
126129
self.cursor = self.conn.cursor()
127130

@@ -136,9 +139,11 @@ def lifetime(self, item_lifetime=0):
136139
def close(self):
137140
self.conn.close()
138141

142+
139143
class Sessions(BasicDatabase):
140144
name = 'session'
141145

146+
142147
class OneTimeKeys(BasicDatabase):
143148
name = 'otk'
144149

roundup/backends/sessions_redis.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@
1818
"""
1919
__docformat__ = 'restructuredtext'
2020

21-
import logging, marshal, redis, time
21+
import marshal, redis, time
2222

2323
from roundup.anypy.html import html_escape as escape
2424

2525
from roundup.i18n import _
2626

27+
from roundup.backends.sessions_common import SessionCommon
2728

28-
class BasicDatabase:
29+
30+
class BasicDatabase(SessionCommon):
2931
''' Provide a nice encapsulation of a redis store.
3032
3133
Keys are id strings, values are automatically marshalled data.
@@ -185,9 +187,9 @@ def set(self, infoid, **newvalues):
185187
transaction.execute()
186188
break
187189
except redis.Exceptions.WatchError:
188-
logging.getLogger('roundup.redis').info(
190+
self.log_info(
189191
_('Key %(key)s changed in %(name)s db' %
190-
{"key": escape(infoid), "name": self.name})
192+
{"key": escape(infoid), "name": self.name})
191193
)
192194
else:
193195
raise Exception(_("Redis set failed afer 3 retries"))

roundup/backends/sessions_sqlite.py

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,53 +11,33 @@
1111
provide a performance speedup.
1212
"""
1313
__docformat__ = 'restructuredtext'
14-
import os, time, logging
1514

16-
from roundup.anypy.html import html_escape as escape
17-
import roundup.backends.sessions_rdbms as rdbms_session
15+
from roundup.backends import sessions_rdbms
1816

19-
class BasicDatabase(rdbms_session.BasicDatabase):
17+
18+
class BasicDatabase(sessions_rdbms.BasicDatabase):
2019
''' Provide a nice encapsulation of an RDBMS table.
2120
2221
Keys are id strings, values are automatically marshalled data.
2322
'''
2423
name = None
24+
2525
def __init__(self, db):
2626
self.db = db
2727
self.conn, self.cursor = self.db.sql_open_connection(dbname=self.name)
2828

29-
self.sql('''SELECT name FROM sqlite_master WHERE type='table' AND name='%ss';'''%self.name)
29+
self.sql('''SELECT name FROM sqlite_master WHERE type='table' AND '''
30+
'''name='%ss';''' % self.name)
3031
table_exists = self.cursor.fetchone()
3132

3233
if not table_exists:
3334
# create table/rows etc.
3435
self.sql('''CREATE TABLE %(name)ss (%(name)s_key VARCHAR(255),
35-
%(name)s_value TEXT, %(name)s_time REAL)'''%{"name":self.name})
36-
self.sql('CREATE INDEX %(name)s_key_idx ON %(name)ss(%(name)s_key)'%{"name":self.name})
36+
%(name)s_value TEXT, %(name)s_time REAL)''' % {"name": self.name})
37+
self.sql('CREATE INDEX %(name)s_key_idx ON '
38+
'%(name)ss(%(name)s_key)' % {"name": self.name})
3739
self.commit()
3840

39-
def log_debug(self, msg, *args, **kwargs):
40-
"""Log a message with level DEBUG."""
41-
42-
logger = self.get_logger()
43-
logger.debug(msg, *args, **kwargs)
44-
45-
def log_info(self, msg, *args, **kwargs):
46-
"""Log a message with level INFO."""
47-
48-
logger = self.get_logger()
49-
logger.info(msg, *args, **kwargs)
50-
51-
def get_logger(self):
52-
"""Return the logger for this database."""
53-
54-
# Because getting a logger requires acquiring a lock, we want
55-
# to do it only once.
56-
if not hasattr(self, '__logger'):
57-
self.__logger = logging.getLogger('roundup')
58-
59-
return self.__logger
60-
6141
def sql(self, sql, args=None, cursor=None):
6242
""" Execute the sql with the optional args.
6343
"""
@@ -69,9 +49,11 @@ def sql(self, sql, args=None, cursor=None):
6949
else:
7050
cursor.execute(sql)
7151

52+
7253
class Sessions(BasicDatabase):
7354
name = 'session'
7455

56+
7557
class OneTimeKeys(BasicDatabase):
7658
name = 'otk'
7759

0 commit comments

Comments
 (0)