Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 53 additions & 99 deletions check_maxscale
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Author: Fabien Hochstrasser <fabien.hochstrasser@swisscom.com>
# Date: 2018-04-03
# Purpose: Nagios check to monitor the MaxScale SQL proxy.
#
# This script uses the maxadmin CLI to read the status of MaxScale.
# This script uses the maxctrl CLI to read the status of MaxScale.
#
# Usage: see --help
#
import argparse
import os
import subprocess
import sys

from collections import namedtuple

Server = namedtuple('Server', 'name address port connections status')
Expand All @@ -34,19 +33,19 @@ def main(args):
problems = []

try:
servers = maxadmin_list_servers()
sessions = maxadmin_list_sessions()
monitors = maxadmin_list_monitors()
servers = maxctrl_list_servers()
sessions = maxctrl_list_sessions()
monitors = maxctrl_list_monitors()
except subprocess.CalledProcessError:
problems.append(Problem(
message='MaxScale doesn\'t seem to be running',
message="MaxScale doesn't seem to be running",
severity=CRITICAL,
))

problems.extend(
check_servers(servers, args.expected_number_of_servers, args.connections_threshold) + \
check_sessions(sessions, args.sessions_threshold) + \
check_monitors(monitors)
check_servers(servers, args.expected_number_of_servers, args.connections_threshold)
+ check_sessions(sessions, args.sessions_threshold)
+ check_monitors(monitors)
)

if args.verbose:
Expand All @@ -64,31 +63,33 @@ def main(args):
print('')
print('Problems')
for problem in problems:
print problem
print(problem)

print_message(problems)
sys.exit(
CRITICAL if any([p.severity == CRITICAL for p in problems])
else WARNING if any([p.severity == WARNING for p in problems])
CRITICAL if any(p.severity == CRITICAL for p in problems)
else WARNING if any(p.severity == WARNING for p in problems)
else OK
)


def maxadmin_list_servers():
"""Return the servers known to MaxScale.

The command 'maxadmin list servers' is used.
"""
servers = []

def _run_maxctrl_list(what):
devnull = open(os.devnull, 'w')
for line in subprocess.check_output(
['maxadmin', 'list', 'servers'],
out = subprocess.check_output(
['maxctrl', '--tsv', 'list', what],
stderr=devnull,
close_fds=True,
).split('\n'):
)
if isinstance(out, bytes):
out = out.decode('utf-8', errors='replace')
return out.split('\n')


def maxctrl_list_servers():
"""Return the servers known to MaxScale."""
servers = []
for line in _run_maxctrl_list('servers'):
try:
fields = [field.strip() for field in line.split('|')]
fields = [field.strip() for field in line.split('\t')]
servers.append(Server(
name=fields[0],
address=fields[1],
Expand All @@ -97,105 +98,77 @@ def maxadmin_list_servers():
status={s.strip() for s in fields[4].split(',')},
))
except (ValueError, IndexError):
# If the line cannot be parsed, just ignore it. It's a header line.
pass

return servers


def maxadmin_list_sessions():
"""Return the sessions established to MaxScale.

The command 'maxadmin list sessions' is used.
"""
def maxctrl_list_sessions():
"""Return the sessions established to MaxScale."""
sessions = []

devnull = open(os.devnull, 'w')
for line in subprocess.check_output(
['maxadmin', 'list', 'sessions'],
stderr=devnull,
close_fds=True,
).split('\n'):
for line in _run_maxctrl_list('sessions'):
try:
fields = [field.strip() for field in line.split('|')]
fields = [field.strip() for field in line.split('\t')]
sessions.append(Session(
id=int(fields[0]),
client=fields[1],
service=fields[2],
state=fields[3],
))
except (ValueError, IndexError):
# If the line cannot be parsed, just ignore it. It's a header line.
pass

return sessions


def maxadmin_list_monitors():
"""Return the monitors configured in MaxScale.

The command 'maxadmin list monitors' is used.
"""
def maxctrl_list_monitors():
"""Return the monitors configured in MaxScale."""
monitors = []

devnull = open(os.devnull, 'w')
for line in subprocess.check_output(
['maxadmin', 'list', 'monitors'],
stderr=devnull,
close_fds=True,
).split('\n'):
for line in _run_maxctrl_list('monitors'):
try:
fields = [field.strip() for field in line.split('|')]
# Ignore the header line
fields = [field.strip() for field in line.split('\t')]
if fields[1] != 'Status':
monitors.append(Monitor(
name=fields[0],
status=fields[1],
))
except (ValueError, IndexError):
# If the line cannot be parsed, just ignore it. It's a header line.
pass

return monitors


def check_servers(servers, expected_number_of_servers, connections_threshold):
problems = []
problems.append(check_servers_count(len(servers), expected_number_of_servers))
problems.append(check_servers_no_master(servers))

for server in servers:
problems.append(check_server_status(server))
problems.append(check_server_connections(server, connections_threshold))

return [p for p in problems if p is not None]


def check_servers_count(real, expected):
"""Return a problem if the number of backend servers is different than expected."""
if real == 0:
return Problem(
message='Could not find any backend server',
severity=CRITICAL,
)
if real != expected:
return Problem(
message='{real} backend servers detected (expected {expected})'.format(
real=real,
expected=expected,
),
severity=WARNING,
)
"""Return a problem if the number of backend servers is different than expected."""
if real == 0:
return Problem(
message='Could not find any backend server',
severity=CRITICAL,
)
if real != expected:
return Problem(
message='{real} backend servers detected (expected {expected})'.format(
real=real,
expected=expected,
),
severity=WARNING,
)


def check_servers_no_master(servers):
"""Return a problem if no backend server has the status "Master"."""
if not any(['Master' in s.status for s in servers]):
if not any('Master' in s.status for s in servers):
return Problem(
message='No backend server in Master mode',
severity=CRITICAL,
)

return None


Expand All @@ -206,19 +179,16 @@ def check_server_status(server):
message='Server "{0}" in Maintenance mode'.format(server.name),
severity=WARNING,
)

if 'Down' in server.status:
return Problem(
message='Server "{0}" down'.format(server.name),
severity=WARNING,
)

if 'Synced' not in server.status:
return Problem(
message='Server "{0}" not Synced'.format(server.name),
severity=CRITICAL,
)

return None


Expand All @@ -232,56 +202,43 @@ def check_server_connections(server, connections_threshold):
),
severity=WARNING,
)

return None


def check_sessions(sessions, sessions_threshold):
problems = []

problems.append(check_sessions_count(len(sessions), sessions_threshold))
problems.append(check_sessions_unique_id(sessions))

return [p for p in problems if p is not None]


def check_sessions_count(real, max):
def check_sessions_count(real, max_sessions):
"""Return a problem if the number of sessions is outside the "normal" range."""
if real > max:
if real > max_sessions:
return Problem(
message='{real} sessions detected (max {max})'.format(
real=real,
max=max,
max=max_sessions,
),
severity=WARNING,
)

return None


def check_sessions_unique_id(sessions):
"""Alert if 2 sessions have the same ID.

From MaxScale documentation:
The sessions are referenced using ID values, these are actually memory address,
however the important thing is that no two session have the same ID.
"""
# Compare list and set sizes. If they differ, there are duplicates
"""Alert if 2 sessions have the same ID."""
if len([s.id for s in sessions]) != len({s.id for s in sessions}):
return Problem(
message='Some sessions have the same ID',
severity=WARNING,
)

return None


def check_monitors(monitors):
problems = []

for monitor in monitors:
problems.append(check_monitor_status(monitor))

return [p for p in problems if p is not None]


Expand All @@ -292,7 +249,6 @@ def check_monitor_status(monitor):
message='Status of monitor "{0}": {1}'.format(monitor.name, monitor.status),
severity=WARNING,
)

return None


Expand All @@ -308,7 +264,6 @@ def print_message(problems):
print('OK - No problem detected on MaxScale')


# Those thresholds are just a guess, they are maybe way to low/high...
DEFAULT_CONNECTIONS_THRESHOLD = 500
DEFAULT_SESSIONS_THRESHOLD = 1000

Expand Down Expand Up @@ -347,5 +302,4 @@ if __name__ == '__main__':
help='Print detailed information',
)
args = parser.parse_args()

main(args)