Skip to content
config.py 7.33 KiB
Newer Older
Seblu's avatar
Seblu committed
# -*- python -*-
Seblu's avatar
Seblu committed
# -*- coding: utf-8 -*-
# This file is part of Installsystems.
# Installsystems 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 3 of the License, or
# (at your option) any later version.
# Installsystems 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 Installsystems.  If not, see <http://www.gnu.org/licenses/>.
Seblu's avatar
Seblu committed

'''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
InstallSystems configuration files module
Seblu's avatar
Seblu committed
'''

from argparse import Namespace
from configobj import ConfigObj, flatten_errors
Sébastien Luttringer's avatar
Sébastien Luttringer committed
from installsystems.exception import ISWarning, ISError
from installsystems.printer import warn, debug
from installsystems.repository.config import RepositoryConfig
from os import access, mkdir, getuid, R_OK, W_OK, X_OK
from os.path import join, expanduser, isfile, basename, abspath, exists, isdir
from sys import argv
from validate import Validator
Seblu's avatar
Seblu committed

Seblu's avatar
Seblu committed
class ConfigFile(object):
Seblu's avatar
Seblu committed
    '''
    Configuration File base class
    '''
Seblu's avatar
Seblu committed

Seblu's avatar
Seblu committed
    def __init__(self, filename):
        '''
        Filename can be full path to config file or a name in config directory
Seblu's avatar
Seblu committed
        '''
        # try to get filename in default config dir
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        if isfile(filename):
            self.path = abspath(filename)
Seblu's avatar
Seblu committed
        else:
Seblu's avatar
Seblu committed
            self.path = self._config_path(filename)
        # loading config file if exists
        if self.path is None:
            raise ISWarning("No config file to load")
        self.config = ConfigObj(self.path, configspec=self.configspec,
                                encoding="utf8", file_error=True)
        self.validate()

    def validate(self):
        '''
        Validate the configuration file according to the configuration specification
        If some values doesn't respect specification, she's ignored and a warning is issued.
        '''
        res = self.config.validate(Validator(), preserve_errors=True)
        # If everything is fine, the validation return True
        # Else, it returns a list of (section, optname, error)
        if res is not True:
            for section, optname, error in flatten_errors(self.config, res):
                # If error is False, this mean no value as been supplied,
                # so we use the default value
                # Else, the check has failed
                if error:
                    warn("%s: %s Skipped" % (optname, error))
                    # remove wrong value to avoid merging it with argparse value
                    del self.config[section[0]][optname]
Seblu's avatar
Seblu committed

Sébastien Luttringer's avatar
Sébastien Luttringer committed
    @staticmethod
    def _config_path(name):
Seblu's avatar
Seblu committed
        '''
        Return path of the best config file
        '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        for cfp in [join(expanduser(u"~/.config/installsystems/%s.conf" %name)),
         u"/etc/installsystems/%s.conf" % name ]:
            if (isfile(cfp) and access(cfp, R_OK)):
                return cfp
Seblu's avatar
Seblu committed
        return None

Sébastien Luttringer's avatar
Sébastien Luttringer committed
    @property
    def configspec(self):
        '''Return configobj spec'''
        raise NotImplementedError()

Seblu's avatar
Seblu committed

class MainConfigFile(ConfigFile):
    '''
    Program configuration class
Seblu's avatar
Seblu committed
    '''

Sébastien Luttringer's avatar
Sébastien Luttringer committed
    def __init__(self, filename, prefix=basename(argv[0])):
Seblu's avatar
Seblu committed
        self.prefix = prefix
Seblu's avatar
Seblu committed
        try:
            super(MainConfigFile, self).__init__(filename)
            debug(u"Loading main config file: %s" % self.path)
        except ISWarning:
            debug("No main config file to load")
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        except Exception as exc:
            raise ISError(u"Unable load main config file %s" % self.path, exc)
Seblu's avatar
Seblu committed

Seblu's avatar
Seblu committed
    def _cache_paths(self):
        '''
        List all candidates to cache directories. Alive or not
        '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        dirs = [expanduser("~/.cache"), "/var/tmp", "/tmp"]
Aurélien Dunand's avatar
Aurélien Dunand committed
        # we have an additional directory if we are root
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        if getuid() == 0:
Seblu's avatar
Seblu committed
            dirs.insert(0, "/var/cache")
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        return [ join(x, self.prefix) for x in dirs ]
Seblu's avatar
Seblu committed

    def _cache_path(self):
        '''
        Return path of the best cache directory
        '''
        # find a good directory
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        for directory in self._cache_paths():
            if (exists(directory)
                and isdir(directory)
                and access(directory, R_OK|W_OK|X_OK)):
                return directory
Seblu's avatar
Seblu committed
        return None

Seblu's avatar
Seblu committed
    @property
    def cache(self):
Seblu's avatar
Seblu committed
        '''
        Find a cache directory
        '''
Seblu's avatar
Seblu committed
        if self._cache_path() is None:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            for directory in self._cache_paths():
Seblu's avatar
Seblu committed
                try:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                    mkdir(directory)
Seblu's avatar
Seblu committed
                    break
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                except Exception as exc:
                    debug(u"Unable to create %s: %s" % (directory, exc))
Seblu's avatar
Seblu committed
        return self._cache_path()

    def parse(self, namespace=None):
        '''
        Parse current loaded option within a namespace
        '''
        if namespace is None:
            namespace = Namespace()
        if self.path:
            for option, value in self.config[self.prefix].items():
                setattr(namespace, option, value)
        return namespace

Sébastien Luttringer's avatar
Sébastien Luttringer committed
    @property
    def configspec(self):
        '''Return configobj spec'''
        return (MAIN_CONFIG_SPEC % self.cache).splitlines()

Seblu's avatar
Seblu committed

class RepoConfigFile(ConfigFile):
    '''
    Repository Configuration class
    '''

    def __init__(self, filename):
Seblu's avatar
Seblu committed
        # seting default config
        self._config = {}
        self._repos = []
        try:
            super(RepoConfigFile, self).__init__(filename)
            debug(u"Loading repository config file: %s" % self.path)
            self._parse()
        except ISWarning:
            debug("No repository config file to load")

    def _parse(self):
        '''
        Parse repositories from config
        '''
Seblu's avatar
Seblu committed
        try:
            # each section is a repository
            for rep in self.config.sections:
Seblu's avatar
Seblu committed
                # check if its a repo section
                if "path" not in self.config[rep]:
Seblu's avatar
Seblu committed
                    continue
                # get all options in repo
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                self._repos.append(
                    RepositoryConfig(rep, **dict(self.config[rep].items()))
                )
        except Exception as exc:
            raise ISError(u"Unable to load repository file %s" % self.path, exc)
Seblu's avatar
Seblu committed

Seblu's avatar
Seblu committed
    @property
    def repos(self):
Seblu's avatar
Seblu committed
        '''
        Get a list of repository available
        '''
        # deep copy
        return list(self._repos)
Sébastien Luttringer's avatar
Sébastien Luttringer committed

    @property
    def configspec(self):
        '''Return configobj spec'''
        return REPO_CONFIG_SPEC.splitlines()


# This must not be an unicode string, because configobj don't decode configspec
# with the provided encoding
MAIN_CONFIG_SPEC = '''\
[installsystems]
verbosity = integer(0, 2)
repo_config = string
repo_search = string
repo_filter = string
repo_timeout = integer
cache = string(default=%s)
timeout = integer
no_cache = boolean
no_check = boolean
no-sync = boolean
no_color = boolean
nice = integer
ionice_class = option("none", "rt", "be", "idle")
ionice_level = integer
'''

# This must not be an unicode string, because configobj don't decode configspec
# with the provided encoding
REPO_CONFIG_SPEC = '''\
[__many__]
    path = string
    fmod = string
    dmod = string
    uid = string
    gid = string
    offline = boolean
    lastpath = string
    dbpath = string
'''