# Copyright (C) 2012 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # # Authors: Jan Safranek # Authors: Michal Minar # -*- coding: utf-8 -*- """ Module for BaseConfiguration class. BaseConfiguration -------------------- .. autoclass:: BaseConfiguration :members: """ import ConfigParser import logging import os import socket from lmi.base.singletonmixin import Singleton def convert_value(section, option, convert_func, value): """ Return result of application of ``convert_func`` on value. If the conversion failes, error is logged and ValueError is raised. :param section: (``str``) Section of configuration file. Used for error message. :param option: (``str``) Option of configuration file. Used for error message. :param convert_func: (``type``) Conversion function to apply on passed value. :param value: (``basestring``) Value to convert. """ if not isinstance(value, basestring): raise TypeError("value must be a string") try: if convert_func is bool: return value.lower() in ('1', 'y', 'yes', 'on', 'true') if convert_func is str and isinstance(value, unicode): return value.encode('utf-8') if convert_func is unicode and isinstance(value, str): return value.decode('utf-8') return convert_func(value) except ValueError as exc: logging.getLogger(__name__).error( 'failed to convert value of "[%s]%s: %s', section, option, exc) raise class BaseConfiguration(Singleton): """ OpenLMI configuration file. By default, it resides in /etc/openlmi/${provider_prefix}/${provider_prefix}.conf. There should be only one instance of this class. """ #: Allow to access global configuration object also from lmi.providers #: subpackage where ``get_instance()`` can not be used. #: This variable shall be set in ``__init__()`` method. INSTANCE = None CONFIG_DIRECTORY_TEMPLATE_TOPLEVEL = '/etc/openlmi/' CONFIG_DIRECTORY_TEMPLATE_PROVIDER = '/etc/openlmi/%(provider_prefix)s/' CONFIG_FILE_PATH_TEMPLATE_TOPLEVEL = \ CONFIG_DIRECTORY_TEMPLATE_TOPLEVEL + 'openlmi.conf' CONFIG_FILE_PATH_TEMPLATE_PROVIDER = \ CONFIG_DIRECTORY_TEMPLATE_PROVIDER + '%(provider_prefix)s.conf' PERSISTENT_PATH_TEMPLATE = '/var/lib/openlmi-%(provider_prefix)s/' SETTINGS_DIR = 'settings/' DEFAULT_OPTIONS = { 'Namespace' : 'root/cimv2', 'SystemClassName' : 'PG_ComputerSystem', # Default logging level "Level" : "ERROR", 'DebugBlivet' : 'false', 'Stderr' : 'false', } @classmethod def provider_prefix(cls): """ This is responsibility of a subclass. :rtype: (``string`) Prefix of providers in lowercase. For example configuration class for storage providers would return "storage". Result is used to construct configuration paths. """ raise NotImplementedError @classmethod def default_options(cls): """ :rtype: (``dict``) Dictionary of default values. """ return cls.DEFAULT_OPTIONS @classmethod def config_directory_toplevel(cls): """ Base directory with toplevel configuration settings. """ return cls.CONFIG_DIRECTORY_TEMPLATE_TOPLEVEL @classmethod def config_directory_provider(cls): """ Base directory with provider specific configuration settings. """ return cls.CONFIG_DIRECTORY_TEMPLATE_PROVIDER % { 'provider_prefix' : cls.provider_prefix() } @classmethod def persistent_path(cls): """ Base directory with persistent settings. """ return cls.PERSISTENT_PATH_TEMPLATE % { 'provider_prefix': cls.provider_prefix() } @classmethod def config_file_path_toplevel(cls): """ File path of toplevel configuration file. """ return cls.CONFIG_FILE_PATH_TEMPLATE_TOPLEVEL @classmethod def config_file_path_provider(cls): """ File path of provider specific configuration file. """ return cls.CONFIG_FILE_PATH_TEMPLATE_PROVIDER % { 'provider_prefix' : cls.provider_prefix() } @classmethod def mandatory_sections(cls): """ Return list of sections, that must be present in configuration file. If not present, they will be created in memory. """ return ['Log', 'CIM'] def __init__(self): """ Initialize and load a configuration file.""" self._listeners = set() self.config = ConfigParser.SafeConfigParser( defaults=self.default_options()) self.load() BaseConfiguration.INSTANCE = self def add_listener(self, callback): """ Add a callback, which will be called when configuration is updated. The callback will be called with instance of this class as parameter: callback(config) """ self._listeners.add(callback) def remove_listener(self, callback): """ Remove previously registered callback. """ self._listeners.remove(callback) def _call_listeners(self): """ Call all listeners that configuration has updated. """ for callback in self._listeners: callback(self) def load(self): """ Load configuration from config files. Provider specific options overrides toplevel openlmi configuration. The files do not need to exist. """ self.config.read([self.config_file_path_toplevel(), self.config_file_path_provider()]) for section in self.mandatory_sections(): if not self.config.has_section(section): self.config.add_section(section) self._call_listeners() @property def namespace(self): """ Return namespace of OpenLMI provider. """ return self.config.get('CIM', 'Namespace') @property def system_class_name(self): """ Return SystemClassName of OpenLMI provider. """ return self.config.get('CIM', 'SystemClassName') @property def logging_level(self): """ Return name of logging level in lower case. """ return self.config.get('Log', 'Level').lower() @property def stderr(self): """ Return True if logging to stderr is enabled. """ return self.config.getboolean('Log', 'Stderr') def file_path(self, section, option): """ Return absolute file path for requested option. Relative path is converted to absolute one with config's directory as a prefix. """ path = self.config.get(section, option) if not os.path.isabs(path): path = os.path.join(self.config_directory(), path) return path def get_safe(self, section, option, convert=str, fallback=None, *args, **kwargs): """ Get the configuration option value as specified type in a safe way. Value is searched in this order: config_file -> defaults_dict -> fallback :param section: (``str``) Section name of option. :param option: (``str``) Option name. :param convert: (``type``) Is a conversion function for obtained value. If the value could not be converted, error message is generated and ``fallback`` is returned. This function is not applied to ``fallback`` value. Supported values are: str, unicode, int ,float, long, bool :param fallback: Value returned, when section or option does not exists and no default value is given, or when the obtained value could not be converted by supplied function. All the other parameters are passed to the ``SafeConfigParser.get()`` method. """ if not isinstance(section, basestring): raise TypeError('section must be a string') if not isinstance(option, basestring): raise TypeError("option must be a string") if not convert in (str, unicode, int, float, long, bool): raise ValueError("unsupported type for conversion: %s:", getattr(convert, '__name__', 'unknown')) if ( not self.config.has_option(section, option) and not option.lower() in self.default_options()): logging.getLogger(__name__).warn( 'no option value and no default supplied for "[%s]%s"', section, option) return fallback try: value = self.config.get(section, option, *args, **kwargs) except ConfigParser.Error as exc: logging.getLogger(__name__).error( 'failed to get value of "[%s]%s": %s', section, option, exc) return fallback try: # first try to convert value from config return convert_value(section, option, convert, value) except ValueError as exc: logging.getLogger(__name__).error( 'failed to convert value of "[%s]%s: %s', section, option, exc) # if it failes, try the value from defaults if ( option.lower() in self.default_options() and self.default_options()[option.lower()] != value): try: return convert_value(section, option, convert, self.default_options()[option.lower()]) except ValueError: pass # error is already logged, no more options left return fallback