Skip to content
Snippets Groups Projects
configuration.py 9.40 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': '/home/brian_checks/venv/get-interface-stats',
            'config': '/var/lib/sensu/conf/get-interface-stats.config.json',
            'command': '{script} --config {config} --juniper {hostname}',
        },
        'gws-direct-interface-check': {
            'script': '/var/lib/sensu/bin/poll-gws-direct.sh',
            'measurement': 'gwsd_counters',
            'command': '{script} --inventory http://localhost:18080'
                       ' {measurement} {nren} {isp} {hostname} {tag}'
        },
        'dscp32-service-check': {
            'script': '/var/lib/sensu/bin/poll-gws-indirect.sh',
            'measurement': 'dscp32_counters',
            'command': '{script} {measurement} {service}'
        },
        'eumetsat-multicast-check': {
          'script': '/home/brian_checks/venv/eumetsat-multicast',
          'measurement': 'multicast',
          'command': '{script} --inventory http://localhost:18080'
                     ' --measurement {measurement} --hostname {hostname}'
        }
    },
    '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'},
                'command': {'type': 'string'},
            },
            'required': ['script', 'measurement', 'command'],
            'additionalProperties': False
        },
        'router-check': {
            'type': 'object',
            'properties': {
                'script': {'type': 'string'},
                'config': {'type': 'string'},
                'command': {'type': 'string'},
            },
            'required': ['script', 'config', 'command'],
            'additionalProperties': False
        },
        'sensu': {
            'type': 'object',
            'properties': {
                'api-base': {
                    'type': 'array',
                    'items': {'type': 'string'},
                    'minItems': 1
                },
                'api-key': {'type': 'string'},
                'interface-check':
                    {'$ref': '#/definitions/router-check'},
                'gws-direct-interface-check':
                    {'$ref': '#/definitions/influx-check'},
                'dscp32-service-check':
                    {'$ref': '#/definitions/influx-check'},
                'eumetsat-multicast-check':
                    {'$ref': '#/definitions/influx-check'},
            },
            'required': [
                'api-base', 'api-key',
                'interface-check',
                'gws-direct-interface-check',
                'dscp32-service-check',
                'eumetsat-multicast-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):

    GWS_DIRECT = 'gws-direct.json'
    GWS_INDIRECT = 'gws-indirect.json'
    INTERFACES = 'interfaces.json'
    STATE = 'state.json'
    EUMET_MC = 'eumetsat-multicast.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.cache_filenames = {
            'state': os.path.join(state_dir, State.STATE),
            'interfaces': os.path.join(state_dir, State.INTERFACES),
            'gws-direct': os.path.join(state_dir, State.GWS_DIRECT),
            'gws-indirect': os.path.join(state_dir, State.GWS_INDIRECT),
            'eumetsat-multicast': os.path.join(state_dir, State.EUMET_MC)
        }

    @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

    @staticmethod
    def _save_json(filename, new_data, schema):
        try:
            jsonschema.validate(new_data, schema)
        except jsonschema.ValidationError:
            logger.exception('invalid interface state data')
            return

        with open(filename, 'w') as f:
            f.write(json.dumps(new_data))

    @property
    def last(self) -> int:
        state = State._load_json(
            self.cache_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.cache_filenames['state'])
        else:
            State._save_json(
                self.cache_filenames['state'],
                {'last': new_last},
                State.STATE_SCHEMA)

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

    @interfaces.setter
    def interfaces(self, new_interfaces):
        State._save_json(
            self.cache_filenames['interfaces'],
            new_interfaces,
            inventory.INVENTORY_INTERFACES_SCHEMA)

    @property
    def gws_direct(self) -> list:
        return State._load_json(
            self.cache_filenames['gws-direct'],
            inventory.GWS_DIRECT_SCHEMA)

    @gws_direct.setter
    def gws_direct(self, new_interfaces):
        State._save_json(
            self.cache_filenames['gws-direct'],
            new_interfaces,
            inventory.GWS_DIRECT_SCHEMA)

    @property
    def gws_indirect(self) -> list:
        return State._load_json(
            self.cache_filenames['gws-indirect'],
            inventory.GWS_INDIRECT_SCHEMA)

    @gws_indirect.setter
    def gws_indirect(self, new_services):
        State._save_json(
            self.cache_filenames['gws-indirect'],
            new_services,
            inventory.GWS_INDIRECT_SCHEMA)

    @property
    def eumetsat_multicast(self) -> list:
        return State._load_json(
            self.cache_filenames['eumetsat-multicast'],
            inventory.MULTICAST_SUBSCRIPTION_LIST_SCHEMA)

    @eumetsat_multicast.setter
    def eumetsat_multicast(self, new_subscriptions):
        State._save_json(
            self.cache_filenames['eumetsat-multicast'],
            new_subscriptions,
            inventory.MULTICAST_SUBSCRIPTION_LIST_SCHEMA)


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