#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright 2021-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
#
import os
from pwd import getpwnam
from contextlib import contextmanager
import json
from six.moves import dbm_gnu as gdbm
from univention.ldap_cache.cache.backend import Caches, LdapCache, Shard, _s
from univention.ldap_cache.log import log, debug
MAX_FAIL_COUNT = 5
[docs]class GdbmCaches(Caches):
def _add_sub_cache(self, name, single_value, reverse):
db_file = os.path.join(self._directory, '%s.db' % name)
debug('Using GDBM %s', name)
cache = GdbmCache(name, single_value, reverse)
cache.db_file = db_file
self._caches[name] = cache
return cache
[docs]class GdbmCache(LdapCache):
def __init__(self, *args, **kwargs):
self.fail_count = 0
super(GdbmCache, self).__init__(*args, **kwargs)
log('%s - Recreating!', self.name)
def _fix_permissions(self):
listener_uid = getpwnam('listener').pw_uid
os.chown(self.db_file, listener_uid, -1)
os.chmod(self.db_file, 0o640)
[docs] @contextmanager
def writing(self, writer=None):
if writer is not None:
yield writer
else:
if not os.path.exists(self.db_file):
self.clear()
writer = gdbm.open(self.db_file, 'csu')
try:
yield writer
finally:
writer.close()
reading = writing
[docs] def save(self, key, values):
with self.writing() as writer:
if self.reverse:
for value in values:
current = self.get(value, writer) or []
if key in current:
continue
debug('%s - Adding %s %r', self.name, value, key)
current.append(key)
writer[value] = json.dumps(current)
else:
self.delete(key, values, writer)
if not values:
return
debug('%s - Saving %s %r', self.name, key, values)
if self.single_value:
writer[key] = values[0]
else:
writer[key] = json.dumps(values)
[docs] def clear(self):
log('%s - Clearing whole DB!', self.name)
gdbm.open(self.db_file, 'nu').close()
self._fix_permissions()
[docs] def cleanup(self):
with self.writing() as db:
try:
db.reorganize()
except gdbm.error:
if self.fail_count > MAX_FAIL_COUNT:
raise
self.fail_count += 1
log('%s - Cleaning up DB FAILED %s times', self.name, self.fail_count)
else:
log('%s - Cleaning up DB WORKED', self.name)
self.fail_count = 0
self._fix_permissions()
[docs] def delete(self, key, values, writer=None):
debug('%s - Delete %s', self.name, key)
with self.writing(writer) as writer:
if self.reverse:
for value in values:
current = self.get(value, writer) or []
try:
current.remove(key)
except ValueError:
continue
writer[value] = json.dumps(current)
else:
try:
del writer[key]
except KeyError:
pass
def __iter__(self):
with self.reading() as reader:
key = _s(reader.firstkey())
while key is not None:
yield key, self.get(key, reader)
key = _s(reader.nextkey(key))
[docs] def get(self, key, reader=None):
with self.reading(reader) as reader:
try:
value = reader[key]
except KeyError:
if self.single_value:
return None
return []
if self.single_value:
return _s(value)
elif value:
return _s(json.loads(value))
[docs] def load(self):
debug('%s - Loading', self.name)
return dict(self)
[docs]class GdbmShard(Shard):
key = 'dn'