Skip to content
Snippets Groups Projects
configuration.py 6.88 KiB
import json
import logging
import logging.config
import os
from typing import Union

import jsonschema

from brian_polling_manager import inventory

logger = logging.getLogger(__name__)

_DEFAULT_LOGGING_FILENAME = os.path.join(
    os.path.dirname(__file__),
    'logging_default_config.json')

_DEFAULT_CONFIG = {
    'inventory': [
        'http://test-inventory-provider01.geant.org:8080',
        'http://test-inventory-provider02.geant.org:8080'
    ],
    'sensu': {
        'api-base': [
            'https://test-poller-sensu-agent01.geant.org:8080',
            'https://test-poller-sensu-agent02.geant.org:8080',
            'https://test-poller-sensu-agent03.geant.org:8080'
        ],
        'api-key': '696a815c-607e-4090-81de-58988c83033e',
        'interface-check': {
            'script': '/var/lib/sensu/bin/counter2influx.sh',
            'measurement': 'counters',
            'interval': 300,
            'subscriptions': ['interfacecounters'],
            'output_metric_handlers': ['influx-db-handler'],
            'namespace': 'default',
            'round_robin': True,
            'command': ('{script} {measurement} '
                        '{community} {hostname} '
                        '{interface} {ifIndex}'),
        }
    },
    'statedir': '/tmp/',
    'logging': _DEFAULT_LOGGING_FILENAME,
    'statsd': {
        'hostname': 'localhost',
        'port': 8125,
        'prefix': 'brian_polling'
    }
}

CONFIG_SCHEMA = {
    '$schema': 'http://json-schema.org/draft-07/schema#',
    'definitions': {
        'influx-check': {
            'type': 'object',
            'properties': {
                'script': {'type': 'string'},
                'measurement': {'type': 'string'},
                'interval': {'type': 'integer'},
                'subscriptions': {
                    'type': 'array',
                    'items': {'type': 'string'}
                },
                'output_metric_handlers': {
                    'type': 'array',
                    'items': {'type': 'string'}
                },
                'namespace': {'type': 'string'},
                'round_robin': {'type': 'boolean'},
                'command': {'type': 'string'},
            },
            'required': ['script', 'measurement', 'interval',
                         'subscriptions', 'output_metric_handlers',
                         'namespace', 'round_robin', 'command'],
            'additionalProperties': False
        },
        'sensu': {
            'type': 'object',
            'properties': {
                'api-base': {
                    'type': 'array',
                    'items': {'type': 'string'},
                    'minItems': 1
                },
                'api-key': {'type': 'string'},
                'interface-check': {'$ref': '#/definitions/influx-check'}
            },
            'required': ['api-base', 'api-key', 'interface-check'],
            'additionalProperties': False
        },
        'statsd': {
            'type': 'object',
            'properties': {
                'hostname': {'type': 'string'},
                'port': {'type': 'integer'},
                'prefix': {'type': 'string'}
            },
            'required': ['hostname', 'port', 'prefix'],
            'additionalProperties': False
        }
    },
    'type': 'object',
    'properties': {
        'inventory': {
            'type': 'array',
            'items': {'type': 'string'},
            'minItems': 1
        },
        'sensu': {'$ref': '#/definitions/sensu'},
        'statedir': {'type': 'string'},
        'logging': {'type': 'string'},
        'statsd': {'$ref': '#/definitions/statsd'}
    },
    'required': ['inventory', 'sensu', 'statedir'],
    'additionalProperties': False
}


class State(object):

    INTERFACES = 'interfaces.json'
    STATE = 'state.json'

    STATE_SCHEMA = {
        '$schema': 'http://json-schema.org/draft-07/schema#',
        'type': 'object',
        'properties': {
            'last': {'type': 'number'}
        },
        'required': ['last'],
        'additionalProperties': False
    }

    def __init__(self, state_dir: str):
        assert os.path.isdir(state_dir)
        self.filenames = {
            'state': os.path.join(state_dir, State.STATE),
            'cache': os.path.join(state_dir, State.INTERFACES)
        }

    @staticmethod
    def _load_json(filename, schema):
        try:
            with open(filename) as f:
                state = json.loads(f.read())
                jsonschema.validate(state, schema)
                return state
        except (json.JSONDecodeError, jsonschema.ValidationError, OSError):
            logger.exception(
                f'unable to open state file {filename}')
            return None

    @property
    def last(self) -> int:
        state = State._load_json(self.filenames['state'], State.STATE_SCHEMA)
        return state['last'] if state else -1

    @last.setter
    def last(self, new_last: Union[float, None]):
        if not new_last or new_last < 0:
            os.unlink(self.filenames['state'])
        else:
            state = {'last': new_last}
            with open(self.filenames['state'], 'w') as f:
                f.write(json.dumps(state))

    @property
    def interfaces(self) -> list:
        return State._load_json(
            self.filenames['cache'],
            inventory.INVENTORY_INTERFACES_SCHEMA)

    @interfaces.setter
    def interfaces(self, new_interfaces):
        try:
            jsonschema.validate(
                new_interfaces,
                inventory.INVENTORY_INTERFACES_SCHEMA)
        except jsonschema.ValidationError:
            logger.exception('invalid interface state data')
            return

        with open(self.filenames['cache'], 'w') as f:
            f.write(json.dumps(new_interfaces))


def _setup_logging(filename=None):
    """
    set up logging using the configured filename

    :raises: json.JSONDecodeError, OSError, AttributeError,
        ValueError, TypeError, ImportError
    """
    if not filename:
        filename = _DEFAULT_LOGGING_FILENAME

    with open(filename) as f:
        # TODO: this mac workaround should be removed ...
        d = json.loads(f.read())
        import platform
        if platform.system() == 'Darwin':
            d['handlers']['syslog_handler']['address'] = '/var/run/syslog'
        logging.config.dictConfig(d)
        # logging.config.dictConfig(json.loads(f.read()))


def load_config(file=None):
    """
    loads, validates and returns configuration parameters

    :param value: filename (file-like object, opened for reading)
    :return: a dict containing configuration parameters
    :raises: json.JSONDecodeError, jsonschema.ValidationError,
        OSError, AttributeError, ValueError, TypeError, ImportError
    """
    if not file:
        config = _DEFAULT_CONFIG
    else:
        config = json.loads(file.read())
        jsonschema.validate(config, CONFIG_SCHEMA)

    _setup_logging(config.get('logging', None))

    return config