#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2018             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.
import datetime

RESTART_POLICIES_TO_DISCOVER = ("always",)

HEALTH_STATUS_MAP = {
    "healthy": 0,
    "starting": 1,
    "unhealthy": 2,
}


def _is_active_container(parsed):
    '''return wether container is or should be running'''
    if parsed.get("Status") == "running":
        return True
    restart_policy_name = parsed.get("RestartPolicy", {}).get("Name")
    return restart_policy_name in RESTART_POLICIES_TO_DISCOVER


def parse_docker_container_status(info):
    '''process the first line to a JSON object

    In case there are multiple lines of output sent by the agent only process the first
    line. We assume that this a full JSON object. The rest of the section is skipped.
    When a container got piggyback data from multiple hosts (e.g. a cluster) this results
    in multiple JSON objects handed over to this check.
    '''
    version = docker_get_version(info)  # pylint: disable=undefined-variable

    index = 0 if version is None else 1
    parsed = docker_json_get_obj(info[index]) or {} if info[index:] else {}  # pylint: disable=undefined-variable

    if version is None:
        return DeprecatedDict(parsed)  # pylint: disable=undefined-variable
    return parsed


#.
#   .--Health--------------------------------------------------------------.
#   |                    _   _            _ _   _                          |
#   |                   | | | | ___  __ _| | |_| |__                       |
#   |                   | |_| |/ _ \/ _` | | __| '_ \                      |
#   |                   |  _  |  __/ (_| | | |_| | | |                     |
#   |                   |_| |_|\___|\__,_|_|\__|_| |_|                     |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   | Represents the containers internal status, as implemented within     |
#   | the container itself using Docker's HEALTHCHECK API                  |
#   '----------------------------------------------------------------------'


def discover_docker_container_status_health(parsed_extra):
    parsed = parsed_extra[0]
    if not _is_active_container(parsed):
        return
    # Only discover if a healthcheck is configured.
    # Stopped containers may have the 'Health' key anyway, so that's no criteria.
    if "Healthcheck" in parsed:
        yield None, {}


def check_docker_container_status_health(_no_item, _no_params, parsed_extra):
    parsed = parsed_extra[0]
    if parsed.get("Status") != "running":
        raise MKCounterWrapped("Container is not running")

    health = parsed.get("Health", {})

    health_status = health.get("Status", "unknown")
    state = HEALTH_STATUS_MAP.get(health_status, 3)
    yield state, "Health status: %s" % health_status.title()

    last_log = health.get("Log", [{}])[-1]
    health_report = last_log.get("Output", "no output").strip()
    if health_report:
        yield int(last_log.get("ExitCode") != 0), "Last health report: %s" % health_report

    if state == 2:
        failing_streak = parsed.get("Health", {}).get("FailingStreak", "not found")
        yield 2, "Failing streak: %s" % failing_streak

    health_test = parsed.get("Healthcheck", {}).get("Test")
    if health_test:
        yield 0, "Health test: %r" % ' '.join(health_test)


check_info["docker_container_status.health"] = {
    "inventory_function": discover_docker_container_status_health,
    "check_function": check_docker_container_status_health,
    "service_description": "Docker container health",
    'includes': ['docker.include', 'legacy_docker.include'],
}

#.
#   .--Status - Main Check-------------------------------------------------.
#   |                    ____  _        _                                  |
#   |                   / ___|| |_ __ _| |_ _   _ ___                      |
#   |                   \___ \| __/ _` | __| | | / __|                     |
#   |                    ___) | || (_| | |_| |_| \__ \                     |
#   |                   |____/ \__\__,_|\__|\__,_|___/                     |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   | Represents the status of the docker container "from the outside"     |
#   '----------------------------------------------------------------------'


def _docker_container_host_labels(parsed):
    yield (u"cmk/docker_object", u"container")

    image_tags = parsed.get("ImageTags")
    if not image_tags:
        return

    image = image_tags[-1]
    yield (u"cmk/docker_image", u"%s" % image)
    if '/' in image:
        __, image = image.rsplit('/', 1)
    if ':' in image:
        image_name, image_version = image.rsplit(':', 1)
        yield (u"cmk/docker_image_name", u"%s" % image_name)
        yield (u"cmk/docker_image_version", u"%s" % image_version)
    else:
        yield (u"cmk/docker_image_name", u"%s" % image)


def discover_docker_container_status(parsed_extra):
    parsed = parsed_extra[0]
    for label, value in _docker_container_host_labels(parsed):
        yield HostLabel(label, value)
    if _is_active_container(parsed):
        yield None, {}


@append_deprecation_warning  # pylint: disable=undefined-variable
def check_docker_container_status(_no_item, _no_params, parsed_extra):
    parsed = parsed_extra[0]
    status = parsed.get("Status", "unknown")
    state = {"running": 0, "unknown": 3}.get(status, 2)

    info = "Container %s" % status
    node_name = parsed.get("NodeName")
    if node_name:
        info += " on node %s" % node_name

    yield state, info

    if parsed.get("Error"):
        yield 2, "Error: %s" % parsed["Error"]


check_info['docker_container_status'] = {
    'parse_function': parse_docker_container_status,
    'inventory_function': discover_docker_container_status,
    'check_function': check_docker_container_status,
    'service_description': 'Docker container status',
    # needed to decide wether we must discover .uptime subcheck:
    'extra_sections': ['uptime'],
    'includes': ['docker.include', 'legacy_docker.include'],
}

#   .--Uptime--------------------------------------------------------------.
#   |                  _   _       _   _                                   |
#   |                 | | | |_ __ | |_(_)_ __ ___   ___                    |
#   |                 | | | | '_ \| __| | '_ ` _ \ / _ \                   |
#   |                 | |_| | |_) | |_| | | | | | |  __/                   |
#   |                  \___/| .__/ \__|_|_| |_| |_|\___|                   |
#   |                       |_|                                            |
#   +----------------------------------------------------------------------+
#   |                                                                      |
#   '----------------------------------------------------------------------'
#.


def discover_docker_container_status_uptime(parsed_extra):
    parsed, agent_uptime_section = parsed_extra
    if agent_uptime_section:
        # if the uptime section of the checkmk agent is
        # present, we don't need this service.
        return []
    if _is_active_container(parsed) and "StartedAt" in parsed:
        return [(None, {})]


def check_docker_container_status_uptime(_no_item, params, parsed_extra):
    parsed, __ = parsed_extra
    started_str = parsed.get("StartedAt")
    if not started_str:
        return

    # assumed format: 2019-06-05T08:58:06.893459004Z
    utc_start = datetime.datetime.strptime(started_str[:-4] + 'UTC', '%Y-%m-%dT%H:%M:%S.%f%Z')

    op_status = parsed["Status"]
    if op_status == "running":
        uptime_sec = (datetime.datetime.utcnow() - utc_start).total_seconds()
        yield check_uptime_seconds(params, uptime_sec)
    else:
        state, _text, perf = check_uptime_seconds(params, 0.)
        yield state, "[%s]" % op_status, perf


check_info["docker_container_status.uptime"] = {
    "inventory_function": discover_docker_container_status_uptime,
    "check_function": check_docker_container_status_uptime,
    "service_description": "Uptime",
    "includes": ['docker.include', 'uptime.include'],
    "group": "uptime",
}
