Skip to content
Snippets Groups Projects
Commit ab0fb71f authored by Erik Reid's avatar Erik Reid
Browse files

get inventory interfaces, save state

parent 69115dae
Branches
Tags
No related merge requests found
import json import json
import logging import logging
import os
import re import re
import click import click
import jsonschema import jsonschema
from brian_polling_manager import sensu from brian_polling_manager import sensu, inventory
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -21,7 +22,8 @@ _DEFAULT_CONFIG = { ...@@ -21,7 +22,8 @@ _DEFAULT_CONFIG = {
'https://test-poller-sensu-agent03.geant.org:8080' 'https://test-poller-sensu-agent03.geant.org:8080'
], ],
'token': '696a815c-607e-4090-81de-58988c83033e' 'token': '696a815c-607e-4090-81de-58988c83033e'
} },
'statedir': '/tmp/'
} }
CONFIG_SCHEMA = { CONFIG_SCHEMA = {
...@@ -46,13 +48,76 @@ CONFIG_SCHEMA = { ...@@ -46,13 +48,76 @@ CONFIG_SCHEMA = {
'items': {'type': 'string'}, 'items': {'type': 'string'},
'minItems': 1 'minItems': 1
}, },
'sensu': {'$ref': '#/definitions/sensu'} 'sensu': {'$ref': '#/definitions/sensu'},
'statedir': {'type': 'string'}
}, },
'required': ['inventory', 'sensu'], 'required': ['inventory', 'sensu', 'statedir'],
'additionalProperties': False '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: float):
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 _validate_config(ctx, param, value): def _validate_config(ctx, param, value):
""" """
loads, validates and returns configuration parameters loads, validates and returns configuration parameters
...@@ -79,6 +144,14 @@ def _validate_config(ctx, param, value): ...@@ -79,6 +144,14 @@ def _validate_config(ctx, param, value):
return config return config
def load_ifc_checks(sensu_params):
def _is_ifc_check(check):
name = check['metadata']['name']
return re.match(r'^check-([^-]+\.geant\.net)-(.+)$', name)
ifc_checks = filter(_is_ifc_check, sensu.load_all_checks(sensu_params))
return {c['metadata']['name']:c for c in ifc_checks}
@click.command() @click.command()
@click.option( @click.option(
'--config', '--config',
...@@ -86,35 +159,27 @@ def _validate_config(ctx, param, value): ...@@ -86,35 +159,27 @@ def _validate_config(ctx, param, value):
type=click.STRING, type=click.STRING,
callback=_validate_config, callback=_validate_config,
help='configuration filename') help='configuration filename')
def main(config): @click.option(
'--force/--no-force',
default=False,
help="update even if inventory hasn't been updated")
def main(config, force):
state = State(config['statedir'])
last = inventory.last_update_timestamp(config['inventory'])
if force or last != state.last:
interfaces = inventory.load_interfaces(config['inventory'])
state.last = last
state.interfaces = interfaces
assert interfaces
def _categorize_ifc_check(check):
name = check['metadata']['name']
m = re.match(r'^check-([^-]+\.geant\.net)-(.+)$', name)
if not m:
return None
return {
'hostname': m.group(1),
'interface': m.group(2),
'check': check
}
ifc_checks = {} ifc_checks = load_ifc_checks(config['sensu'])
for check in filter(None, map(_categorize_ifc_check, sensu.load_all_checks(config['sensu']))): print(ifc_checks.keys())
host_checks = ifc_checks.setdefault(check['hostname'], {})
if check['interface'] in host_checks:
other_check = host_checks[check['interface']]
other_name = other_check['metadata']['name']
this_name = check['metadata']['name']
logger.warning(f'found both {this_name} and {other_name}')
host_checks[check['interface']] = check['check']
for host, interfaces in ifc_checks.items():
print(host)
for ifc, check in interfaces.items():
name = check['metadata']['name']
print(f'\t{ifc}: {name}')
print('here')
if __name__ == '__main__': if __name__ == '__main__':
main() logging.basicConfig(level=logging.DEBUG)
\ No newline at end of file main()
import logging
import random
import jsonschema
import requests
logger = logging.getLogger(__name__)
# minimal inventory response schema
INVENTORY_VERSION_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'latch': {
'type': 'object',
'properties': {
'timestamp': {'type': 'number'}
},
'required': ['timestamp'],
'additionalProperties': True
}
},
'type': 'object',
'properties': {
'latch': {'$ref': '#/definitions/latch'}
},
'required': ['latch'],
'additionalProperties': True
}
INVENTORY_INTERFACES_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'service': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
'type': {'type': 'string'},
'status': {'type': 'string'},
},
'required': ['id', 'name', 'type', 'status'],
'additionalProperties': False
},
'interface': {
'type': 'object',
'properties': {
'router': {'type': 'string'},
'name': {'type': 'string'},
'description': {'type': 'string'},
'snmp-index': {
'type': 'integer',
'minimum': 1
},
'bundle': {
'type': 'array',
'items': {'type': 'string'}
},
'bundle-parents': {
'type': 'array',
'items': {'type': 'string'}
},
'circuits': {
'type': 'array',
'items': {'$ref': '#/definitions/service'}
}
},
'required': [
'router', 'name', 'description',
'snmp-index', 'bundle', 'bundle-parents',
'circuits'],
'additionalProperties': False
},
},
'type': 'array',
'items': {'$ref': '#/definitions/interface'}
}
def _pick_one(things):
if not isinstance(things, (list, tuple, set)):
things = [things]
return random.choice(things)
def load_interfaces(base_urls):
"""
Load /poller/interfaces from inventory provider
and return a slightly reformatted dict.
:param base_urls: inventory provider base api url, or a list of them
:return: a dict like [<router>][<interface>] = inventory leaf data
"""
url = _pick_one(base_urls)
logger.debug(f'using inventory base api url: {url}')
rsp = requests.get(
f'{url}/poller/interfaces',
headers={'Accept': 'application/json'})
rsp.raise_for_status()
result = rsp.json()
jsonschema.validate(result, INVENTORY_INTERFACES_SCHEMA)
return result
def last_update_timestamp(base_urls) -> float:
try:
r = requests.get(
f'{_pick_one(base_urls)}/version',
headers={'Accept': 'application/json'})
r.raise_for_status()
result = r.json()
jsonschema.validate(result, INVENTORY_VERSION_SCHEMA)
return result['latch']['timestamp']
except (IOError, jsonschema.ValidationError, ValueError):
logger.exception('connection error')
return None
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment