#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2014             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software;  you can redistribute it and/or modify it
# under the  terms of the  GNU General Public License  as published by
# the Free Software Foundation in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# ails.  You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

# Example outputs from agent:
# <<<heartbeat_crm>>>
# ============
# Last updated: Thu Jul  1 07:48:19 2010
# Current DC: mwp (118cc1e7-bbf3-4550-b820-cac372885be1)
# 2 Nodes configured.
# 2 Resources configured.
# ============
# Node: smwp (2395453b-d647-48ff-a908-a7cd76062265): online
# Node: mwp (118cc1e7-bbf3-4550-b820-cac372885be1): online
# Full list of resources:
# Resource Group: group_slapmaster
#     resource_virtip1  (ocf::heartbeat:IPaddr):  Started mwp
#     resource_virtip2  (ocf::heartbeat:IPaddr):  Started mwp
#     resource_pingnodes  (ocf::heartbeat:pingd): Started mwp
#     resource_slapmaster (ocf::heartbeat:OpenLDAP):  Started mwp
# resource_slapslave  (ocf::heartbeat:OpenLDAP):  Started smwp

import time, datetime

# Nails down the DC to the node which is the DC during inventory. The check
# will report CRITICAL when another node becomes the DC during later checks.
# If set to "False" the check will be passed.
#
# Leave this option to be compatible with inventorized pre 1.2.5i6
heartbeat_crm_naildown = True

# Max age of "last updated"
#
# Leave this option to be compatible with inventorized pre 1.2.5i6
heartbeat_crm_default_max_age = 60

# Naildown the resources to the nodes which care about the resources during
# the inventory run
#
# Leave this option to be compatible with inventorized pre 1.2.5i6
heartbeat_crm_resources_naildown = True

# Holds a dict of settings which tell the inventory functions whether or not
# some options like the resources and DC role shal be nailed down to the
# node which holds these resources during inventory.
inventory_heartbeat_crm_rules = []

factory_settings["heartbeat_crm_default_levels"] = {
    "max_age" : heartbeat_crm_default_max_age,
}

def heartbeat_crm_parse_general(info):
    dc, num_nodes, num_resources = None, None, None
    for line in info:
        if ' '.join(line[0:2]) == 'Last updated:':
            last_updated = ' '.join(line[2:])
        elif ' '.join(line[0:2]) == 'Current DC:':
            dc = line[2]
        elif ' '.join(line[1:3]).rstrip('.,') == 'Nodes configured':
            num_nodes = int(line[0])
        elif ' '.join(line[1:3]).rstrip('.,') == 'Resources configured':
            num_resources = int(line[0])
    return (last_updated, dc, num_nodes, num_resources)

def inventory_heartbeat_crm(info):
    # Use these lines to gather the inventory and perform this check:
    # ============
    # Last updated: Thu Jul  1 07:48:19 2010
    # Current DC: mwp (118cc1e7-bbf3-4550-b820-cac372885be1)
    # 2 Nodes configured.
    # 2 Resources configured.
    # ============
    #
    # - Naildown the DC or not.
    # - Check the number of nodes/resources
    # - Check the age of "last updated"
    settings = host_extra_conf_merged(g_hostname, inventory_heartbeat_crm_rules)
    last_updated, dc, num_nodes, num_resources = heartbeat_crm_parse_general(info)
    params = {
        'num_nodes'     : num_nodes,
        'num_resources' : num_resources,
    }
    if settings.get('naildown_dc', False):
        params['dc'] = dc
    return [(None, params)]

def check_heartbeat_crm(item, params, info):
    if len(info) > 0:
        last_updated, dc, numNodes, numResources = heartbeat_crm_parse_general(info)

        # Convert old tuple params (pre 1.2.5i6)
        if type(params) == tuple:
            params = {
                'max_age'       : params[0],
                'dc'            : params[1] != "" and params[1] or None,
                'num_nodes'     : params[2] != -1 and params[2] or None,
                'num_resources' : params[3] != -1 and params[3] or None,
            }

        # Check the freshness of the crm_mon output and terminate with CRITICAL
        # when too old information are found
        dt = int(datetime.datetime(*time.strptime(last_updated, '%a %b %d %H:%M:%S %Y')[:6]).strftime("%s"))
        now = time.time()
        delta = now - dt
        if delta > params['max_age']:
            return 3, 'Ignoring reported data (Status output too old: %d secs)' % delta

        output = ''
        status = 0

        # Check for correct DC when enabled
        if params.get('dc') == None or dc == params['dc']:
            output += 'DC: %s, ' % dc
        else:
            output += 'DC: %s (Expected %s (!!)), ' % (dc, params['dc'])
            status = 2

        # Check for number of nodes when enabled
        if params['num_nodes'] != None:
            if numNodes == params['num_nodes']:
                output += 'Nodes: %d, ' % numNodes
            else:
                output += 'Nodes: %d (Expected %d (!!)), ' % (numNodes, params['num_nodes'])
                status = 2

        # Check for number of resources when enabled
        if params['num_resources'] != None:
            if numResources == params['num_resources']:
                output += 'Resources: %d, ' % numResources
            else:
                output += 'Resources: %d (Expected %d (!!)), ' % (numResources, params['num_ressources'])
                status = 2

        return (status, output.rstrip(', '))

check_info["heartbeat_crm"] = {
    'check_function'          : check_heartbeat_crm,
    'inventory_function'      : inventory_heartbeat_crm,
    'service_description'     : 'Heartbeat CRM General',
    'group'                   : 'heartbeat_crm',
    'default_levels_variable' : 'heartbeat_crm_default_levels',
}

def heartbeat_crm_parse_resources(info):
    blockStart = False
    resources = {}
    resource = ''
    mode = 'single'
    for line in info:
        if ' '.join(line) == 'Failed actions:':
            blockStart = False
        elif not blockStart and ' '.join(line) == 'Full list of resources:':
            blockStart = True
        elif blockStart:
            if ' '.join(line[0:2]) == 'Resource Group:':
                # Resource group
                resources[line[-1]] = []
                resource = line[-1]
                mode = 'resourcegroup'
            elif ' '.join(line[0:2]) == 'Clone Set:':
                # Clone set
                resources[line[-2]] = []
                resource = line[-2]
                mode = 'cloneset'
            elif ' '.join(line[0:2]) == 'Master/Slave Set:':
                # Master/Slave set
                resources[line[-2]] = []
                resource = line[-2]
                mode = 'masterslaveset'
            elif line[0] == '_':
                # Resource group or set member
                if mode == 'resourcegroup':
                    resources[resource].append(line[1:])
                elif mode == 'cloneset':
                    if line[1] == 'Started:':
                        resources[resource].append([ resource, 'Clone', 'Started', line[3] ])
                elif mode == 'masterslaveset':
                    if line[1] == 'Masters:':
                        resources[resource].append([ resource, 'Master', 'Started', line[3] ])
                    if line[1] == 'Slaves:':
                        resources[resource].append([ resource, 'Slave', 'Started', line[3] ])
            else:
                # Single resource
                resources[line[0]] = [ line ]

    return resources

def inventory_heartbeat_crm_resources(info):
    # Full list of resources:
    # Resource Group: group_slapmaster
    #     resource_virtip1  (ocf::heartbeat:IPaddr):  Started mwp
    #     resource_virtip2  (ocf::heartbeat:IPaddr):  Started mwp
    #     resource_pingnodes  (ocf::heartbeat:pingd): Started mwp
    #     resource_slapmaster (ocf::heartbeat:OpenLDAP):  Started mwp
    # resource_slapslave  (ocf::heartbeat:OpenLDAP):  Started smwp
    inventory = []
    settings = host_extra_conf_merged(g_hostname, inventory_heartbeat_crm_rules)
    for name, resources in heartbeat_crm_parse_resources(info).iteritems():
        # In naildown mode only resources which are started somewhere can be
        # inventorized
        if settings.get('naildown_resources', False) and resources[0][2] != 'Stopped':
            inventory.append((name, '"%s"' % resources[0][3]))
        else:
            inventory.append((name, None))
    return inventory

def check_heartbeat_crm_resources(item, target_node, info):
    output = ''
    status = 0
    for resource in [ resources for name, resources in heartbeat_crm_parse_resources(info).iteritems() if name == item ][0]:
        output += ' '.join(resource)
        if len(resource) == 3 or resource[2] != 'Started':
            status = 2
            output += ' (Resource is in state "%s" (!!))' % resource[2]
        elif target_node and target_node != resource[3] and resource[1] != 'Slave' and resource[1] != 'Clone':
            status = 2
            output += ' (Expected node: %s (!!))' % target_node
        output += ', '

    return (status, output.rstrip(', '))

check_info["heartbeat_crm.resources"] = {
    'check_function'      : check_heartbeat_crm_resources,
    'inventory_function'  : inventory_heartbeat_crm_resources,
    'service_description' : 'Heartbeat CRM %s',
    'group'               : 'heartbeat_crm_resources',
}
