#!/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-
# tails. 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.

# no used space check for Tablsspaces with CONTENTS in ('TEMPORARY','UNDO')
# It is impossible to check the used space in UNDO and TEMPORARY Tablespaces
# These Types of Tablespaces are ignored in this plugin.
# This restriction is only working with newer agents, because we need an
# additional parameter at end if each datafile


# This definition needs to be removed at a later stage
# A previous version of this check didn't write the parameter
# name into the autochecks file, but the parameter itself
# default levels for *free* space. float: percent,
# integer: MB.
oracle_tablespaces_default_levels = (10.0, 5.0)

factory_settings["oracle_tablespaces_defaults"] = {
    "levels"           : (10.0, 5.0),
    "magic_normsize"   : 1000,
    "magic_maxlevels"  : (60.0, 50.0),
    "defaultincrement" : True,
}

# Whether to check auto extend settings. Note: this setting is not ment to
# be changed anymore. It cannot be edited via WATO either. There now exists
# a check parameter, where the behaviour can be configured on a per-service-base.
oracle_tablespaces_check_autoext = True

# this parameter is deprecated and needed for old configurations with
# parameter in main.mk. It is not used anymore!
oracle_tablespaces_check_default_increment = True

# <<<oracle_tablespaces>>>
# pengt /database/pengt/daten155/dbf/system_01.dbf SYSTEM AVAILABLE YES 38400 4194302 38392 1280 SYSTEM 8192 ONLINE
# pengt /database/pengt/daten155/dbf/undotbs_01.dbf UNDOTBS1 AVAILABLE YES 128000 4194302 127992 640 ONLINE 8192 ONLINE
# pengt /database/pengt/daten155/dbf/sysaux_01.dbf SYSAUX AVAILABLE YES 25600 4194302 25592 1280 ONLINE 8192 ONLINE
# pengt /database/pengt/daten155/dbf/ts_user_01.dbf TS_USER AVAILABLE YES 8480 1280000 8472 160 ONLINE 8192 ONLINE
# pengt /database/pengt/daten155/dbf/TS_PENG_ABR_01.dbf TS_PENG_ABR AVAILABLE YES 12800 1280000 12792 12800 ONLINE 8192 ONLINE

# invalid data
# <<<oracle_tablespaces>>>
# AIMCOND1|/u00/app/oracle/product/db12010/dbs/MISSING00064|CONRPG_DATA|AVAILABLE||||||OFFLINE|8192|ONLINE|0|PERMANENT
# MAE|/opt/oracle/oracle_base/product/11.2.0.4/dbs/pslife_dwh.dbf|PSLIFE_DWH|AVAILABLE||||||RECOVER|8192|OFFLINE|0|PERMANENT

# Order of columns (it is a table of data files, so table spaces appear multiple times)
# -1 Node info (added by Check_MK)
# 0  database SID
# 1  data file name
# 2  table space name
# 3  status of the data file
# 4  whether the file is auto extensible
# 5  current size of data file in blocks
# 6  maximum size of data file in blocks (if auto extensible)
# 7  currently number of blocks used by user data
# 8  size of next increment in blocks (if auto extensible)
# 9  wheter the file is in use (online)
# 10 block size in bytes
# 11 status of the table space
# 12 free space in the datafile
# 13 Tablespace-Type (PERMANENT, UNDO, TEMPORARY)


def parse_oracle_tablespaces(info):
    tablespaces = {}
    error_sids  = {}

    for line in info:
        node_name = line[0]
        line      = line[1:]

        # Check for query errors
        err = oracle_handle_ora_errors(line)
        if err == False:
            continue # ignore ancient agent outputs
        elif isinstance(err, tuple):
            sid = line[0]
            error_sids[sid] = err

        if len(line) not in (13, 14, 15):
            continue

        sid, datafile_name, ts_name, datafile_status, autoextensible, \
        filesize_blocks, max_filesize_blocks, used_blocks, increment_size, \
        file_online_status, block_size, ts_status, free_space = line[:13]

        db_version = 0

        if len(line) >= 14:
            ts_type = line[13]
        else:
            # old behaivor is all Tablespaces are treated as PERMANENT
            ts_type = 'PERMANENT'

        if len(line) == 15:
            db_version = line[14].split('.')[0]

        tablespaces.setdefault((node_name, sid, ts_name, db_version), [])

        this_tablespace = {
            "name"               : datafile_name,
            "status"             : datafile_status,
            "autoextensible"     : autoextensible == "YES",
            "ts_type"            : ts_type,
            "ts_status"          : ts_status,
            "file_online_status" : file_online_status,
        }

        try:
            bs = int(block_size)
            this_tablespace.update({
                "block_size"         : bs,
                "size"               : int(filesize_blocks) * bs,
                "max_size"           : int(max_filesize_blocks) * bs,
                "used_size"          : int(used_blocks) * bs,
                "free_space"         : int(free_space) * bs,
                "increment_size"     : int(increment_size) * bs,
            })

        except:
            this_tablespace.update({
                "block_size"         : None,
                "size"               : None,
                "max_size"           : None,
                "used_size"          : None,
                "free_space"         : None,
                "increment_size"     : None,
            })

        tablespaces[(node_name, sid, ts_name, db_version)].append(this_tablespace)

    # Now join this into one dictionary. If there are more than
    # one nodes per tablespace, then we select that node with the
    # most data files
    result = {}
    for (node_name, sid, ts_name, db_version), datafiles in tablespaces.items():
        ts_key = (sid, ts_name)
        # Use data from this node, if it is the first/only, or if it
        # has more data files than a previous one
        if ts_key not in result or \
           len(result[ts_key]["datafiles"]) < len(datafiles):

            result[ts_key] = {
                "db_version"     : db_version,
                "datafiles"      : datafiles,
                "type"           : datafiles[0]["ts_type"],
                "status"         : datafiles[0]["ts_status"],
                "autoextensible" : False,
            }

            for df in datafiles:
                if df["autoextensible"]:
                    result[ts_key]["autoextensible"] = True

    return result, error_sids


def inventory_oracle_tablespaces(parsed):
    tablespaces, error_sids = parsed
    for (sid, ts_name), tablespace in tablespaces.items():
        if tablespace["status"] in ( "ONLINE", "READONLY", "OFFLINE" ):
            if oracle_tablespaces_check_autoext:
                ae = tablespace["autoextensible"]
            else:
                ae = None # means: ignore, only display setting

            parameters = { "autoextend" : ae }
            yield "%s.%s" % (sid, ts_name), parameters


def check_oracle_tablespaces(item, params, parsed):
    try:
        sid, ts_name = item.split('.')
    except ValueError:
        yield 3, 'Invalid check item (must be <SID>.<tablespace>)'
        return

    tablespaces, error_sids = parsed
    if sid in error_sids:
        yield error_sids[sid]
        return

    # In case of missing information we assume that the login into
    # the database has failed and we simply skip this check. It won't
    # switch to UNKNOWN, but will get stale.
    # TODO Treatment as in db2 and mssql dbs
    # "ts_status == None" possible?
    tablespace = tablespaces.get((sid, ts_name))
    if not tablespace or tablespace["status"] == None:
        raise MKCounterWrapped("Login into database failed")

    ts_type        = tablespace["type"]
    ts_status      = tablespace["status"]
    db_version     = tablespace["db_version"]
    num_files      = 0
    num_avail      = 0
    num_extensible = 0
    current_size   = 0
    max_size       = 0
    used_size      = 0
    num_increments = 0
    increment_size = 0
    free_space     = 0

    # Conversion of old autochecks params
    if type(params) == tuple:
        params = { "autoextend" : params[0], "levels" : params[1:] }

    autoext = params.get("autoextend")
    uses_default_increment = False

    for datafile in tablespace["datafiles"]:

        if datafile["file_online_status"] in [ "OFFLINE", "RECOVER" ]:
            yield 2, "One or more datafiles OFFLINE or RECOVER"
            return

        num_files += 1
        if datafile["status"] in [ "AVAILABLE", "ONLINE", "READONLY" ]:
            df_size       = datafile["size"]
            df_free_space = datafile["free_space"]
            df_max_size   = datafile["max_size"]

            num_avail    += 1
            current_size += df_size
            used_size    += df_size - df_free_space

            # Autoextensible? Honor max size. Everything is computed in
            # *Bytes* here!
            if datafile["autoextensible"]:
                num_extensible += 1
                incsize         = datafile["increment_size"]

                if df_size > df_max_size:
                    max_size       += df_size
                    free_extension  = df_size - df_max_size # free extension space
                else:
                    max_size       += df_max_size
                    free_extension  = df_max_size - df_size # free extension space

                if incsize == datafile["block_size"]:
                    uses_default_increment = True

                #incs, rest = divmod(free_extension, incsize)
                #if rest:
                #    # ??? Was ist, wenn es nicht genau aufgeht und ein weiteres
                #    # increment max_size überschreiten würde? Das geht ja wohl
                #    # nicht oder??
                #    # incs += 1 ### Dann würde max_size überschritten!
                incs = free_extension / incsize
                num_increments += incs
                increment_size += incsize * incs

                if db_version >= 11:
                    # The size of next extent in datafile is ignored when remaining
                    # free space is > then next extend. Oracle uses all space up to the maximum!
                    free_space += df_max_size - df_size
                else:
                    # The free space in this table is the current free space plus
                    # the additional space that can be gathered by using all available
                    # remaining increments
                    free_space += increment_size + df_free_space

            # not autoextensible: take current size as maximum
            else:
                max_size   += df_size
                free_space += df_free_space

    infotexts = ["%s (%s), Size: %s, %s used (%s of max. %s)" % \
        (ts_status, ts_type, get_bytes_human_readable(current_size),
         get_percent_human_readable(100.0 * used_size / max_size),
         get_bytes_human_readable(used_size),
         get_bytes_human_readable(max_size))]

    if num_extensible > 0 and db_version <= 10:
        # only display the number of remaining extents in Databases <= 10g
        infotexts.append("%d increments (%s)" % \
                        (num_increments, get_bytes_human_readable(increment_size)))

    warn, crit, levels_text, output_as_percentage = \
        db_get_tablespace_levels_in_bytes(max_size, params)

    yield 0, ", ".join(infotexts), \
        [("size", current_size, max_size - (warn or 0), max_size - (crit or 0)),
         ("used", used_size), ("max_size", max_size)]

    # Check increment size, should not be set to default (1)
    if params.get("defaultincrement"):
        if uses_default_increment:
            yield 1, "DEFAULT INCREMENT"

    # Check autoextend status if parameter not set to None
    if autoext is not None:
        if autoext and num_extensible == 0:
            autoext_info = "NO AUTOEXTEND"
        elif not autoext and num_extensible > 0:
            autoext_info = "AUTOTEXTEND"
        else:
            autoext_info = None

        if autoext_info:
            yield params.get("autoextend_severity", 2), autoext_info

    elif num_extensible > 0:
        yield 0, "autoextend"

    else:
        yield 0, "no autoextend"

    # Check free space, but only if status is not READONLY
    # and Tablespace-Type must be PERMANENT
    if ts_status != "READONLY" and ts_type == 'PERMANENT':

        status = 0
        if crit is not None and free_space <= crit:
            status = 2
        elif warn is not None and free_space <= warn:
            status = 1

        if status:
            yield status, "only %s left%s" % (get_bytes_human_readable(free_space), levels_text)

    if num_files != 1 or num_avail != 1 or num_extensible != 1:
        yield 0, "%d data files (%d avail, %d autoext)" % \
                  (num_files, num_avail, num_extensible)


check_info['oracle_tablespaces'] = {
    "parse_function"          : parse_oracle_tablespaces,
    "inventory_function"      : inventory_oracle_tablespaces,
    "check_function"          : check_oracle_tablespaces,
    "service_description"     : "ORA %s Tablespace",
    "has_perfdata"            : True,
    "node_info"               : True,
    "group"                   : "oracle_tablespaces",
    "default_levels_variable" : "oracle_tablespaces_defaults",
    "includes"                : [ "oracle.include", "db.include" ]
}
