Skip to content
Snippets Groups Projects
__init__.py 16.05 KiB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# This file is part of Mentat system (https://mentat.cesnet.cz/).
#
# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/)
# Use of this source is governed by the MIT license, see LICENSE file.
#-------------------------------------------------------------------------------


"""
Description
-----------

This pluggable module provides access to IDEA message database.

"""


__author__ = "Jan Mach <jan.mach@cesnet.cz>"
__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"


import re

import flask
from flask_babel import gettext, lazy_gettext

import mentat.stats.idea
import mentat.services.eventstorage
from mentat.datatype.sqldb import EventStatisticsModel

import hawat.const
import hawat.base
import hawat.events
import hawat.acl
from hawat.blueprints.events.forms import SimpleEventSearchForm, EventDashboardForm

#
# Name of the blueprint as module global constant.
#
BLUEPRINT_NAME = 'events'


def in_query_params(haystack, needles, on_true = True, on_false = False, on_empty = False):
    """
    Utility method for checking that any needle from given list of needles is
    present in given haystack.
    """
    if not haystack:
        return on_empty
    for needle in needles:
        if needle in haystack:
            return on_true
    return on_false

class SearchView(hawat.base.HawatRenderableView):
    """
    View responsible for searching IDEA event database and presenting the results.
    """
    methods = ['GET']

    authentication = True

    authorization = [hawat.acl.PERMISSION_ANY]

    @classmethod
    def get_view_name(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_view_name`.
        """
        return 'search'

    @classmethod
    def get_menu_icon(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_menu_icon`.
        """
        return 'module-{}'.format(BLUEPRINT_NAME)

    @classmethod
    def get_menu_title(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_menu_title`.
        """
        return lazy_gettext('Events')

    @classmethod
    def get_view_title(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatRenderableView.get_view_title`.
        """
        return lazy_gettext('Search IDEA event database')

    @classmethod
    def get_query_parameters(cls, form, request_args):
        """
        This method can be used to get the dictionary containing actual parameters
        that were used to generate and send the request. This dictionary can be
        then used to generate for example paginator links.

        :param wtforms.Form form: Form that was parsed from raw request.
        :param dict request_args: Raw request arguments.
        :return: Dictionary containing intersect of both form and request_args
        :rtype: dict
        """
        params = {}
        for arg in request_args:
            if getattr(form, arg, None) and request_args.get(arg, None):
                params[arg] = request_args[arg]
        return params

    @classmethod
    def search(cls, args, context):
        """
        Perform actual search of IDEA events using provided query arguments.

        :param dict args: Search query arguments.
        :param dict context: Jinja2 template context, that will be passed to template during rendering.
        :return: Tuple containing number of items as integer and list of searched items.
        :rtype: tuple
        """
        items_count_total, items = hawat.events.db_get().search_events(args)
        context.update(sqlquery = hawat.events.db_get().cursor.lastquery)
        return items_count_total, items

    @classmethod
    def get_context_action_menu(cls):
        """
        Get context action menu for particular item.
        """
        action_menu = hawat.menu.HawatMenu()

        action_menu.add_entry(
            'endpoint',
            'show',
            endpoint = 'events.show',
            hidetitle = True,
            legend = lambda x: lazy_gettext('View details of event &quot;%(item)s&quot;', item = x.get_id()),
            item_kw = 'get_id'
        )
        action_menu.add_entry(
            'endpoint',
            'download',
            endpoint = 'events.download',
            icon = 'action-download',
            hidetitle = True,
            legend = lambda x: lazy_gettext('Download event &quot;%(item)s&quot;', item = x.get_id()),
            item_kw = 'get_id'
        )

        return action_menu

    def dispatch_request(self):  # pylint: disable=locally-disabled,too-many-locals
        """
        Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`.
        Will be called by the *Flask* framework to service the request.
        """
        # Get lists of available options for various event search form select fields.
        sourcetypes   = hawat.events.get_event_source_types()
        targettypes   = hawat.events.get_event_target_types()
        hosttypes     = sorted(list(set(sourcetypes + targettypes)))
        detectors     = hawat.events.get_event_detectors()
        detectortypes = hawat.events.get_event_detector_types()
        categories    = hawat.events.get_event_categories()
        severities    = hawat.events.get_event_severities()
        classes       = hawat.events.get_event_classes()
        protocols     = hawat.events.get_event_protocols()

        form = SimpleEventSearchForm(
            flask.request.args,
            meta = {'csrf': False},
            choices_source_types   = list(zip(sourcetypes,sourcetypes)),
            choices_target_types   = list(zip(targettypes,targettypes)),
            choices_host_types     = list(zip(hosttypes,hosttypes)),
            choices_detectors      = list(zip(detectors,detectors)),
            choices_detector_types = list(zip(detectortypes,detectortypes)),
            choices_categories     = list(zip(categories,categories)),
            choices_severities     = list(zip(severities,severities)),
            choices_classes        = list(zip(classes,classes)),
            choices_protocols      = list(zip(protocols,protocols))
        )

        context = self.get_template_context()

        if hawat.const.HAWAT_FORM_ACTION_SUBMIT in flask.request.args:
            if form.validate():
                form_data = form.data
                form_data['groups'] = [item.name for item in form_data['groups']]
                try:
                    items_count_total, items = self.search(form_data, context)

                    context.update(
                        searched = True,
                        items = items,
                        items_count = len(items),
                        items_count_total = items_count_total,
                        pager_index_low = ((form_data['page'] - 1) * form_data['limit']) + 1,
                        pager_index_high = ((form_data['page'] - 1) * form_data['limit']) + len(items),
                        pager_index_limit = ((form_data['page'] - 1) * form_data['limit']) + form_data['limit'],
                        form_data = form_data,
                        context_action_menu = self.get_context_action_menu()
                    )

                except mentat.services.eventstorage.DataError as err:
                    match = re.match('invalid IP4R value: "([^"]+)"', str(err))
                    if match:
                        flask.flash(
                            flask.Markup(
                                gettext(
                                    'Invalid address value <strong>%(address)s</strong> in event search form.',
                                    address = flask.escape(str(match.group(1)))
                                )
                            ),
                            hawat.const.HAWAT_FLASH_FAILURE
                        )
                    else:
                        raise

        context.update(
            search_form = form,
            request_args = flask.request.args,
            query_params = self.get_query_parameters(form, flask.request.args),
            in_query_params = in_query_params
        )
        return flask.render_template(self.get_view_template(), **context)


class ShowView(hawat.base.HawatRenderableView):
    """
    Detailed event view.
    """
    methods = ['GET']

    authentication = True

    authorization = [hawat.acl.PERMISSION_ANY]

    @classmethod
    def get_view_name(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_view_name`.
        """
        return 'show'

    @classmethod
    def get_menu_icon(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_menu_icon`.
        """
        return 'action-show'

    @classmethod
    def get_menu_title(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_menu_title`.
        """
        return lazy_gettext('Show IDEA event')

    @classmethod
    def get_view_title(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatRenderableView.get_view_title`.
        """
        return lazy_gettext('Show IDEA event')

    @classmethod
    def get_action_menu(cls, item_id, item):  # pylint: disable=locally-disabled,unused-argument
        """
        Get action menu for particular item.
        """
        action_menu = hawat.menu.HawatMenu()

        action_menu.add_entry(
            'endpoint',
            'download',
            endpoint = 'events.download',
            title = lazy_gettext('Download'),
            icon = 'action-download',
            legend = lazy_gettext('Download event &quot;%(item)s&quot;', item = item_id),
            link = flask.url_for('events.download', item_id = item_id)
        )

        return action_menu

    #---------------------------------------------------------------------------

    def dispatch_request(self, item_id):  # pylint: disable=locally-disabled,arguments-differ
        """
        Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`.
        Will be called by the *Flask* framework to service the request.

        Single item with given unique identifier will be retrieved from database
        and injected into template to be displayed to the user.
        """
        item = hawat.events.db_get().fetch_event(item_id)
        if not item:
            flask.abort(404)

        context = self.get_template_context()
        context.update(
            item_id = item_id,
            item = item,
            action_menu = self.get_action_menu(item_id, item)
        )

        return flask.render_template(self.get_view_template(), **context)


class DownloadView(hawat.base.HawatBaseView):
    """
    Download event view.
    """
    methods = ['GET']

    authentication = True

    authorization = [hawat.acl.PERMISSION_ANY]

    @classmethod
    def get_view_name(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_view_name`.
        """
        return 'download'

    @classmethod
    def get_menu_icon(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_menu_icon`.
        """
        return 'action-download'

    @classmethod
    def get_menu_title(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_menu_title`.
        """
        return lazy_gettext('Download IDEA event')

    #---------------------------------------------------------------------------

    def dispatch_request(self, item_id):  # pylint: disable=locally-disabled,arguments-differ
        """
        Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`.
        Will be called by the *Flask* framework to service the request.

        Single item with given unique identifier will be retrieved from database
        and injected into template to be displayed to the user.
        """
        item = hawat.events.db_get().fetch_event(item_id)
        if not item:
            flask.abort(404)

        response = flask.make_response(
            item.to_json(indent = 4, sort_keys = True)
        )
        response.mimetype = 'application/json'
        response.headers['Content-Disposition'] = 'attachment; filename={}.idea.json'.format(item_id)
        return response


class DashboardView(hawat.base.HawatSearchView):
    """
    View responsible for presenting overall system performance dashboard.
    """
    authentication = True

    authorization = [hawat.acl.PERMISSION_ANY]

    @classmethod
    def get_view_name(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_view_name`.
        """
        return 'dashboard'

    @classmethod
    def get_menu_icon(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_menu_icon`.
        """
        return 'module-{}'.format(BLUEPRINT_NAME)

    @classmethod
    def get_menu_title(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBaseView.get_menu_title`.
        """
        return lazy_gettext('Events')

    @classmethod
    def get_view_title(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatRenderableView.get_view_title`.
        """
        return lazy_gettext('Overall event dashboards')

    @property
    def dbmodel(self):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatDbmodelView.dbmodel`.
        """
        return EventStatisticsModel

    @staticmethod
    def get_search_form(args):
        return EventDashboardForm(args, meta = {'csrf': False})

    def search(self, query, model, args, context):
        if 'dt_from' in args and args['dt_from']:
            query = query.filter(model.dt_from >= args['dt_from'])
        if 'dt_to' in args and args['dt_to']:
            query = query.filter(model.dt_to <= args['dt_to'])
        return query.order_by(model.interval).all()

    def do_before_render(self, item, context):
        """
        *Hook method*. Will be called before rendering the template.
        """
        context.update(
            statistics = mentat.stats.idea.truncate_evaluations(
                mentat.stats.idea.aggregate_stat_groups(item)
            )
        )


#-------------------------------------------------------------------------------


class EventsBlueprint(hawat.base.HawatBlueprint):
    """
    Hawat pluggable module - IDEA events.
    """

    @classmethod
    def get_module_title(cls):
        """
        *Interface implementation* of :py:func:`hawat.base.HawatBlueprint.get_module_title`.
        """
        return lazy_gettext('IDEA events pluggable module')

    def register_app(self, app):
        """
        *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint`
        method and can be used to customize the Flask application object. Possible
        use cases:

        * application menu customization

        :param hawat.base.HawatApp app: Flask application to be customize.
        """
        app.menu_main.add_entry(
            'view',
            'dashboards.events',
            position = 10,
            view = DashboardView
        )
        app.menu_main.add_entry(
            'view',
            BLUEPRINT_NAME,
            position = 140,
            view = SearchView,
            resptitle = True
        )


#-------------------------------------------------------------------------------


def get_blueprint():
    """
    Mandatory interface and factory function. This function must return a valid
    instance of :py:class:`hawat.base.HawatBlueprint` or :py:class:`flask.Blueprint`.
    """

    hbp = EventsBlueprint(
        BLUEPRINT_NAME,
        __name__,
        template_folder = 'templates',
        url_prefix = '/{}'.format(BLUEPRINT_NAME)
    )

    hbp.register_view_class(SearchView, '/search')
    hbp.register_view_class(ShowView, '/show/<item_id>')
    hbp.register_view_class(DownloadView, '/download/<item_id>')
    hbp.register_view_class(DashboardView, '/dashboard')

    return hbp