# -*- coding: utf-8 -*-
#
# Univention Configuration Registry
# Config Registry information: read information about registered Config Registry
# variables
#
# Copyright 2007-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
import re
import univention.info_tools as uit
try:
from typing import Dict, Iterable, List, Optional, Tuple # noqa: F401
except ImportError:
pass
# default locale
_locale = 'de'
[docs]class Variable(uit.LocalizedDictionary):
"""UCR variable description."""
def __init__(self, registered=True):
# type: (bool) -> None
uit.LocalizedDictionary.__init__(self)
self.value = None # type: Optional[str]
self._registered = registered
[docs] def check(self):
# type: () -> List[str]
"""
Check description for completeness.
:returns: List of missing settings.
"""
missing = [] # type: List[str]
if not self._registered:
return missing
for key in ('description', 'type', 'categories'):
if not self.get(key, None):
missing.append(key)
return missing
[docs]class Category(uit.LocalizedDictionary):
"""UCR category description."""
def __init__(self):
# type: () -> None
uit.LocalizedDictionary.__init__(self)
[docs] def check(self):
# type: () -> List[str]
"""
Check description for completeness.
:returns: List of missing settings.
"""
missing = [] # type: List[str]
for key in ('name', 'icon'):
if not self.get(key, None):
missing.append(key)
return missing
[docs]class ConfigRegistryInfo(object):
"""UCR variable and category descriptions."""
BASE_DIR = '/etc/univention/registry.info'
CATEGORIES = 'categories'
VARIABLES = 'variables'
CUSTOMIZED = '_customized'
FILE_SUFFIX = '.cfg'
def __init__(self, install_mode=False, registered_only=True, load_customized=True):
# type: (bool, bool, bool) -> None
"""
Initialize variable and category descriptions.
:param install_mode: `True` deactivates the use of an UCR instance.
:param registered_only: `False` creates synthetic entries for all undescribed but set variables.
:param load_customized: `False` deactivates loading customized descriptions.
"""
self.categories = {} # type: Dict[str, Category]
self.variables = {} # type: Dict[str, Variable]
self._patterns = {} # type: Dict[str, List[Tuple[str, str]]]
if not install_mode:
import univention.config_registry as ucr # circular import
self._configRegistry = ucr.ConfigRegistry() # type: Optional[ucr.ConfigRegistry]
self._configRegistry.load()
self.load_categories()
self._load_variables(registered_only, load_customized)
else:
self._configRegistry = None
[docs] def check_categories(self):
# type: () -> Dict[str, List[str]]
"""
Check all categories for completeness.
:returns: dictionary of incomplete category descriptions.
"""
incomplete = {} # type: Dict[str, List[str]]
for name, cat in self.categories.items():
miss = cat.check()
if miss:
incomplete[name] = miss
return incomplete
[docs] def check_variables(self):
# type: () -> Dict[str, List[str]]
"""
Check variables.
:returns: dictionary of incomplete variable descriptions.
"""
incomplete = {} # type: Dict[str, List[str]]
for name, var in self.variables.items():
miss = var.check()
if miss:
incomplete[name] = miss
return incomplete
[docs] def read_categories(self, filename):
# type: (str) -> None
"""
Load a single category description file.
:param filename: File to load.
"""
cfg = uit.UnicodeConfig()
cfg.read(filename)
for sec in cfg.sections():
# category already known?
cat_name = sec.lower()
if cat_name in self.categories:
continue
cat = Category()
for name, value in cfg.items(sec):
cat[name] = value
self.categories[cat_name] = cat
[docs] def load_categories(self):
# type: () -> None
"""Load all category description files."""
path = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.CATEGORIES)
if os.path.exists(path):
for filename in os.listdir(path):
self.read_categories(os.path.join(path, filename))
@staticmethod
def _pattern_sorter(args):
# type: (Tuple) -> Tuple[Tuple[int, str], str]
"""Sort more specific (longer) regular expressions first."""
pattern, data = args
return ((len(pattern), pattern), data)
[docs] def check_patterns(self):
# type: () -> None
"""
Match descriptions agains currently defined UCR variables.
"""
# in install mode
if self._configRegistry is None:
return
# Try more specific (longer) regular expressions first
for pattern, data in sorted(self._patterns.items(), key=ConfigRegistryInfo._pattern_sorter, reverse=True):
regex = re.compile(pattern)
# find config registry variables that match this pattern and are
# not already listed in self.variables
for key, value in self._configRegistry.items():
if key in self.variables:
continue
if not regex.match(key):
continue
# create variable object with values
var = Variable()
var.value = value
# var.update() does not use __setitem__()
for name, value in data:
var[name] = value
self.variables[key] = var
[docs] def describe_search_term(self, term):
# type: (str) -> Dict[str, Variable]
"""
Try to apply a description to a search term.
This is not complete, because it would require a complete "intersect
two regular languages" algorithm.
:param term: Search term.
:returns: Dictionary mapping variable pattern to Variable info blocks.
"""
patterns = {} # type: Dict[str, Variable]
for pattern, data in sorted(self._patterns.items(), key=ConfigRegistryInfo._pattern_sorter, reverse=True):
regex = re.compile(pattern)
match = regex.search(term)
if not match:
regex = re.compile(term)
match = regex.search(pattern)
if match:
var = Variable()
# var.update() does not use __setitem__()
for name, value in data:
var[name] = value
patterns[pattern] = var
return patterns
[docs] def write_customized(self):
# type: () -> None
"""Persist the customized variable descriptions."""
filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, ConfigRegistryInfo.CUSTOMIZED)
self._write_variables(filename)
def _write_variables(self, filename=None, package=None):
# type: (str, str) -> bool
"""
Persist the variable descriptions into a file.
:param filename: Explicit filename for saving.
:param package: Explicit package name.
:raises AttributeError: if neither `filename` nor `package` are given.
:returns: `True` on success, `False` otherwise.
"""
if filename:
pass
elif package:
filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, package + ConfigRegistryInfo.FILE_SUFFIX)
else:
raise AttributeError("neither 'filename' nor 'package' is specified")
try:
with open(filename, 'w') as fd:
cfg = uit.UnicodeConfig()
for name, var in self.variables.items():
cfg.add_section(name)
for key in var.keys():
items = var.normalize(key)
for item, value in items.items():
value = value
cfg.set(name, item, value)
cfg.write(fd)
return True
except EnvironmentError:
return False
[docs] def read_customized(self):
# type: () -> None
"""Read customized variable descriptions."""
filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, ConfigRegistryInfo.CUSTOMIZED)
self.read_variables(filename, override=True)
[docs] def read_variables(self, filename=None, package=None, override=False):
# type: (str, str, bool) -> None
"""
Read variable descriptions.
:param filename: Explicit filename for loading.
:param package: Explicit package name.
:param override: `True` to overwrite already loaded descriptions.
:raises AttributeError: if neither `filename` nor `package` are given.
"""
if filename:
pass
elif package:
filename = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES, package + ConfigRegistryInfo.FILE_SUFFIX)
else:
raise AttributeError("neither 'filename' nor 'package' is specified")
cfg = uit.UnicodeConfig()
cfg.read(filename)
for sec in cfg.sections():
# is a pattern?
if sec.find('.*') != -1:
self._patterns[sec] = cfg.items(sec)
continue
# variable already known?
if not override and sec in self.variables:
continue
var = Variable()
for name, value in cfg.items(sec):
var[name] = value
# get current value
if self._configRegistry is not None:
var.value = self._configRegistry.get(sec, None)
self.variables[sec] = var
def _load_variables(self, registered_only=True, load_customized=True):
# type: (bool, bool) -> None
"""
Read default and customized variable descriptions.
:param registered_only: With default `True` only variables for which a description exists are loaded, otherwise all currently set variables are also included.
:param load_customized: Load customized variable descriptions.
"""
path = os.path.join(ConfigRegistryInfo.BASE_DIR, ConfigRegistryInfo.VARIABLES)
if os.path.exists(path):
for entry in os.listdir(path):
cfgfile = os.path.join(path, entry)
if os.path.isfile(cfgfile) and cfgfile.endswith(ConfigRegistryInfo.FILE_SUFFIX) and entry != ConfigRegistryInfo.CUSTOMIZED:
self.read_variables(cfgfile)
self.check_patterns()
if not registered_only and self._configRegistry is not None:
for key, value in self._configRegistry.items():
if key in self.variables:
continue
var = Variable(registered=False)
var.value = value
self.variables[key] = var
# read customized infos afterwards to override existing entries
if load_customized:
self.read_customized()
[docs] def get_categories(self):
# type: () -> Iterable[str]
"""
Return a list of category names.
:returns: List if categories.
"""
return self.categories.keys()
[docs] def get_category(self, name):
# type: (str) -> Optional[Category]
"""
Returns a category object associated with the given name or None.
:param name: Name of the category.
:returns:
"""
if name.lower() in self.categories:
return self.categories[name.lower()]
return None
[docs] def get_variables(self, category=None):
# type: (str) -> Dict[str, Variable]
"""
Return dictionary of variable info blocks belonging to given category.
:param category: Name of the category. `None` defaults to all variables.
:returns: Dictionary mapping variable-name to :py:class:`Variable` instance.
"""
if not category:
return self.variables
temp = {} # type: Dict[str, Variable]
for name, var in self.variables.items():
categories = var.get('categories')
if not categories:
continue
if category in [_.lower() for _ in categories.split(',')]:
temp[name] = var
return temp
[docs] def get_variable(self, key):
# type: (str) -> Optional[Variable]
"""
Return the description of a variable.
:param key: Variable name.
:returns: description object or `None`.
"""
return self.variables.get(key, None)
[docs] def add_variable(self, key, variable):
# type: (str, Variable) -> None
"""
Add a new variable information item or overrides an old entry.
:param key: Variable name.
:param variable: :py:class:`Variable` instance.
"""
self.variables[key] = variable
[docs]def set_language(lang):
# type: (str) -> None
"""Set the default language."""
global _locale
_locale = lang
uit.set_language(lang)