diff --git a/.gitignore b/.gitignore index aed4bd663..d31f51826 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,3 @@ MANIFEST dist adagios.conf *.mo - diff --git a/adagios/context_processors.py b/adagios/context_processors.py index 99f374da0..4c4bf3394 100644 --- a/adagios/context_processors.py +++ b/adagios/context_processors.py @@ -32,6 +32,7 @@ import datetime from adagios import __version__ from adagios import userdata +from adagios.status import custom_columns from django.utils.translation import ugettext as _ @@ -82,6 +83,8 @@ def on_page_load(request): results[k] = v for k, v in get_all_nonworking_backends(request).items(): results[k] = v + for k, v in get_livestatus_datasources(request).items(): + results[k] = v return results @@ -375,5 +378,11 @@ def get_all_nonworking_backends(request): if not Livestatus(x).test(raise_error=False)] return {'nonworking_backends': b} +def get_livestatus_datasources(request): + result = {} + result['livestatus_datasources'] = sorted(custom_columns.all_columns.keys()) + return result + + if __name__ == '__main__': on_page_load(request=None) diff --git a/adagios/media/css/adagios-common.css b/adagios/media/css/adagios-common.css index 81820e9d0..7b07df87c 100644 --- a/adagios/media/css/adagios-common.css +++ b/adagios/media/css/adagios-common.css @@ -311,6 +311,16 @@ button.close.notification { margin-top: 20px; } +#left_sidebar .custom-view-link { + display: inline-block; + width: 70%; +} + +#left_sidebar .custom-view-edit { + display: inline; + width: 10%; +} + .well { background: url('../img/bak.jpg') } diff --git a/adagios/media/js/adagios_status.js b/adagios/media/js/adagios_status.js index b14ab5c60..acdde417f 100644 --- a/adagios/media/js/adagios_status.js +++ b/adagios/media/js/adagios_status.js @@ -739,10 +739,10 @@ adagios.status.reschedule = function() { host_name = item['host_name']; service_description = item['service_description'] || ''; object_type = item['object_type']; - if (object_type == 'host') { + if (object_type == 'host' || object_type == 'hosts') { hostlist = hostlist + ';' + host_name; } - else if (object_type == 'service') { + else if (object_type == 'service' || object_type == 'services') { servicelist = servicelist + ';' + host_name + ',' + service_description; } }); @@ -994,4 +994,4 @@ adagios.objectbrowser.autocomplete_for_multichoicefields = function() { choices = choices.split(','); $(this).select2({tags:choices}); }); -}; \ No newline at end of file +}; diff --git a/adagios/settings.py b/adagios/settings.py index 943abc7ed..7da16f631 100644 --- a/adagios/settings.py +++ b/adagios/settings.py @@ -178,6 +178,12 @@ ] +# Custom views +CUSTOM_TEMPLATES_DIR = 'custom_views/templates/' +# this will be avoided one day with Django 1.7 (render_to_response(..., dirs=[...])) +CUSTOM_TEMPLATES_PATH = '%s/status/templates/%s/' % (djangopath, CUSTOM_TEMPLATES_DIR) +CUSTOM_WIDGETS_PATH = '%s/status/templates/custom_views/widgets/' % djangopath + # Graphite # # the url where to fetch data and images diff --git a/adagios/status/custom_columns.py b/adagios/status/custom_columns.py new file mode 100644 index 000000000..2c4389b7c --- /dev/null +++ b/adagios/status/custom_columns.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# Adagios is a web based Nagios configuration interface +# +# Copyright (C) 2014, Matthieu Caneill +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" +This module looks up and stores Livesatus columns, organised per table, +and stored along with their name, type and description. +Access the data using: +>>> from adagios.status import custom_columns +>>> cols = custom_columns.all_columns + +As a sidenote, columns for a specific table can be obtained with this oneliner: +$ printf 'GET columns' | netcat localhost 50000 | awk -F ';' '{if ($3 == "hosts") {print $2}}' +The Filter clause is not implemented in Shinken for the Columns table, +thus forcing us to post-process the data. +""" + +import os +import json + +from adagios.status import utils + +def get_data(): + """ + Queries Livestatus to get the list of all available columns. + Loops on the backends until one works. + """ + backends = utils.get_all_backends() + livestatus = utils.livestatus(None) + query = 'GET columns' + # we try every backend until one works properly + # otherwise columns appear multiple times, coming from multiple backends + # this is faster than using set() afterwards + res = None + for backend in backends: + try: + res = livestatus.query(query, backend=backend) + break + except: + continue + if not res: + raise Exception('No working backend found.') + return res + +def process_data(data): + """ + Columns list post-processing. + This `unflats` the columns, and returns a dict(), with keys being + the names of the Livestatus tables, and values being lists of + name/description/type dict()s. + """ + cols_by_table = {} + for el in data: + filtered = {'name': el['name'], + 'description': el['description'], + 'type': el['type'], + } + table = el['table'] + if table in cols_by_table.keys(): + if el['name'] not in [x['name'] for x in cols_by_table[table]]: + pass + + try: + cols_by_table[el['table']].append(filtered) + except Exception: + cols_by_table[el['table']] = [filtered] + return cols_by_table + +def get_columns(): + """ + Computes Livestatus columns. + """ + data = get_data() + data = process_data(data) + return data + +# we keep this here, so this is only queried and processed once. +all_columns = get_columns() diff --git a/adagios/status/custom_filters.py b/adagios/status/custom_filters.py new file mode 100644 index 000000000..6137e4b34 --- /dev/null +++ b/adagios/status/custom_filters.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# +# Adagios is a web based Nagios configuration interface +# +# Copyright (C) 2014, Matthieu Caneill +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" +This module manages the different filters subforms used in custom_forms. +A custom filter is a class: + * inheriting from Default + * defining fields + * defining __repr__, its string representation being inserted in the + Livestatus query at filtering time. +The rest is mapping between these classes and Livestatus tables.columns. +""" + +from django import forms +# YAY, Python 2.6 :D (collections.OrderedDict is available from Python 2.7) +from django.utils.datastructures import SortedDict + +class Default(object): + def __init__(self, values={}): + self.values = values + + def __repr__(self): + d = self.values.copy() + d['op'] = '~' if self.values['regex'] else '=' + d['negate'] = '\nNegate:\n' if self.values['negate'] else '' + return 'Filter: %(column)s %(op)s %(value)s%(negate)s\n' % d + + def get_fields(self): + fields = SortedDict([ + ('value', forms.CharField(required=False)), + ('regex', forms.BooleanField(required=False)), + ('negate', forms.BooleanField(required=False)), + ]) + return fields + +class HostStates(Default): + bool_names = ('UP', 'DOWN', 'UNREACHABLE', 'PENDING') + + def __repr__(self): + states = [index + for index, key in enumerate(self.__class__.bool_names) + if self.values[key]] + column = self.values['column'] + q = '' + if len(states) > 0: + q += ''.join(['Filter: %s = %s\n' % (column, index) for index in states]) + if len(states) > 1: + q += 'Or: %d\n' % len(states) + return q + + def get_fields(self): + fields = SortedDict([(x, forms.BooleanField(required=False)) + for x in self.__class__.bool_names]) + return fields + +class ServiceStates(HostStates): + bool_names = ('OK', 'WARNING', 'CRITICAL', 'UNKNOWN') + +class YesNo(Default): + def __repr__(self): + q = 'Filter: %s = %d' % (self.values['column'], int(self.values['value'])) + return q + + def get_fields(self): + fields = SortedDict([('value', forms.ChoiceField( + required=False, + choices=[(1, 'Yes'), (0, 'No')], + widget=forms.widgets.RadioSelect))]) + return fields + +mapping = { + 'hosts': { + 'state': HostStates, + }, + 'services': { + 'state': ServiceStates, + 'host_state': HostStates, + 'acknowledged': YesNo, + } + } + +def get_filter(datasource, column): + """ + Returns the filter corresponding to the given table (datasource) + and column, or the default filter if not defined. + """ + try: + return mapping[datasource][column] + except KeyError: + return Default diff --git a/adagios/status/custom_forms.py b/adagios/status/custom_forms.py new file mode 100644 index 000000000..3a2f8676c --- /dev/null +++ b/adagios/status/custom_forms.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# +# Adagios is a web based Nagios configuration interface +# +# Copyright (C) 2014, Matthieu Caneill +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" +This module defines the forms used in the views engine. + +The forms are dynamically generated, w.r.t. the data being passed, +the available columns in the current Livestatus instance, and +the available templates and filters in the looked-up directories. + +For the configuration, see adagios.settings.CUSTOM_* + +The forms generated here are transformed into formsets by views.py, +which allows to have multiple values. +The data is stored in user.views[], that's being managed +by views.py as well. +""" + +import os + +from adagios.status import custom_columns +from adagios.status import custom_filters + +from django import forms +from django.forms.formsets import formset_factory +from adagios import settings +from django.utils.translation import ugettext as _ + +#NONE_CHOICE = [('', '(None)')] + +def _get_columns_names(datasource='hosts'): + """ + Returns the list of columns (coming from custom_columns) for the + specified table (datasource). + """ + columns = custom_columns.all_columns + s = [x['name'] for x in columns[datasource]] + return [(x,x) for x in s] + +def _get_html_files_in(path): + """ + Returns a list of tuples usable in a select/option field. + The elements are file present in `path` and ending with .html. + The first element (