ceph / ceph/ceph-nagios-plugins

Plugin for RBD mirroring monitoring

Open
#92 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
81
Forks
84
PR merge metrics
No merged PRs in 30d

Description

Hi, I have developed a plugin to monitor the status of RBD snapshot mirroring. What do you think about adding it to the project?

```python
#!/usr/bin/env python3
"""
Nagios plugin to check Ceph RBD snapshot-based mirroring health.

The script connects to a Ceph cluster and inspects all pools with
RBD mirroring enabled in IMAGE mode. For each mirrored image, it
evaluates the mirror state (replaying or stopped) and, when stopped,
parses peer status metadata to detect stale or desynchronized snapshots
based on a configurable age cut-off. The plugin reports OK when all
images are synchronized, or CRITICAL when unsynchronized, stale, or
misconfigured images are detected.

An optional YAML configuration file can be used to exclude images:
- poolExclude: list of pool names to skip.
- excludeRegex: list of regular expressions matched against "pool/image".

Performance data emitted by the plugin includes:

- Number of mirrored images currently synchronized and up
- Number of images in replaying state
- Number of images in stopped state
- Number of images excluded by configuration
- Number of excluded images that are currently up
- Number of images with errors (unsynchronized, stale, or misconfigured)
- Total number of images checked (excluding excluded images)

When invoked with the -m option, the plugin prints the rbd commands
required to enable snapshot-based mirroring for images where mirroring
is currently disabled.

Required Ceph permissions for the client used (e.g., [client.nagios]):
- mon = "allow r"
- osd = "allow r, allow "class-read"

Use the -h option to display all available command-line parameters.
"""

__author__ = 'Juan Ferrer Toribio '
__copyright__ = 'Copyright (c) 2025 Verdnatura Levante SL'
__license__ = 'GPLv3'
__version__ = '1.0.2'

import rados
import rbd
import json
import time
import sys
import argparse
import re
import yaml
import os
from enum import IntEnum

class Status(IntEnum):
OK = 0
WARNING = 1
CRITICAL = 2
UNKNOWN = 3

class PoolModes(IntEnum):
NONE = 0
IMAGE = 1

class ImageState(IntEnum):
UP_REPLAYING = 4
UP_STOPPED = 6

# Parse arguments

parser = argparse.ArgumentParser(
description="Ceph RBD mirror health Nagios plugin."
)
parser.add_argument('-c', '--conf',
help="Alternative ceph conf file",
default='/etc/ceph/ceph.conf',
)
parser.add_argument('-k', '--keyring',
help="Ceph client keyring file",
)
parser.add_argument('-n', '--name',
help="Ceph client name",
)
parser.add_argument('-i', '--id',
help="Ceph client name",
default='admin',
)
parser.add_argument('-t', '--cutoff',
help="Snapshot age cut-off seconds",
default=129600, # 36 hours
type=int,
)
parser.add_argument('-w', '--warn-excluded',
help="Show warning when excluded image is up",
action='store_true',
)
parser.add_argument('-s', '--show-excluded',
help="Show excluded image statuses",
action='store_true',
)
parser.add_argument('-m', '--commands',
help="Display commands to enable mirroring on non-excluded disabled images",
action='store_true',
)
parser.add_argument('-f', '--cconf',
help="Check configuration file",
default='/etc/nagios/check_rbd_mirror.yaml',
)
args = parser.parse_args()

# Fetch configuration

conf = {
'poolExclude': [],
'excludeRegex': [],
}
if os.path.isfile(args.cconf):
with open(args.cconf) as f:
fileConf = yaml.safe_load(f) or {}
conf = {**conf, **fileConf}

pool_exclude = set(conf['poolExclude'] or [])

if conf['excludeRegex']:
exclude_regex = []
for regex in conf['excludeRegex']:
exclude_regex.append(re.compile(regex))
else:
exclude_regex = None

snap_cutoff = int(time.time()) - args.cutoff

# Connect to Ceph cluster

try:
keyring = args.keyring
if not keyring:
keyring = f'/etc/ceph/ceph.client.{args.id}.keyring'
name = args.name
if not name:
name = f'client.{args.id}'
cluster = rados.Rados(
conffile=args.conf,
name=name,
conf=dict(
keyring=keyring,
)
)
cluster.connect()
except Exception as e:
print(f"CRITICAL - Cannot connect to Ceph cluster: {e}")
sys.exit(Status.CRITICAL)

# Iterate over snapshot mirrored pools

try:
n_up = 0
n_excluded_up = 0
n_stopped = 0
n_replaying = 0
n_total = 0
img_check_statuses = {}
img_errors = set()
img_excluded = set()

for pool in cluster.list_pools():
if pool in pool_exclude:
continue

try:
ioctx = cluster.open_ioctx(pool)
rbd_inst = rbd.RBD()
images = rbd_inst.list(ioctx)
except Exception as e:
print(f"CRITICAL - Error opening pool {pool}: {e}")
sys.exit(Status.CRITICAL)

mirror_mode = rbd_inst.mirror_mode_get(ioctx)
if mirror_mode != PoolModes.IMAGE:
ioctx.close()
continue

statuses = rbd_inst.mirror_image_status_list(ioctx)
img_statuses = {status['name']: status for status in statuses}
ioctx.close()

# Iterate over images

for img_name in images:
img_full_name = f'{pool}/{img_name}'

excluded = False
if exclude_regex:
for regex in exclude_regex:
if regex.match(img_full_name):
excluded = True
break

if not excluded:
n_total += 1

img_is_up = False
img_check_status = ''
img_status = img_statuses.get(img_name)

if img_status:
state = img_status.get('state')
if state == ImageState.UP_STOPPED:
peers = img_status.get('remote_statuses', [])
if peers and len(peers) > 0:
description = peers[0].get('description', '')
desc_json_str = description.split(',', 1)[-1].strip()
try:
desc_json = json.loads(desc_json_str)
local_stamp = desc_json.get('local_snapshot_timestamp', 0)
remote_stamp = desc_json.get('remote_snapshot_timestamp', 0)
if remote_stamp < snap_cutoff:
img_check_status = 'snap+desync'
elif local_stamp < snap_cutoff:
img_check_status = 'snap+old'
else:
img_check_status = 'up+stopped'
img_is_up = True
except json.JSONDecodeError:
img_check_status = 'json+invalid'
else:
img_check_status = 'peer+empty'
elif state == ImageState.UP_REPLAYING:
img_check_status = 'up+replaying'
img_is_up = True
else:
img_check_status = 'state+unknown'
else:
img_check_status = 'mirror+disabled'

if excluded:
img_excluded.add(img_full_name)
if img_is_up:
n_excluded_up += 1
elif img_is_up:
n_up += 1
match state:
case ImageState.UP_STOPPED:
n_stopped += 1
case ImageState.UP_REPLAYING:
n_replaying += 1
else:
img_errors.add(img_full_name)

img_check_statuses[img_full_name] = img_check_status
finally:
cluster.shutdown()

# Output results

if args.commands:
for img in sorted(img_errors):
if img_check_statuses[img] == 'mirror+disabled':
print(f'rbd mirror image enable "{img}" snapshot')
sys.exit(0)

n_errors = len(img_errors)
n_excluded = len(img_excluded)

perf_data = (
f'n_up={n_up} n_replaying={n_replaying} n_stopped={n_stopped} '
f'n_excluded={n_excluded} n_excluded_up={n_excluded_up}'
)

if n_errors:
perf_data = f'n_errors={n_errors} n_total={n_total} {perf_data}'
status_text = f"{n_errors} of {n_total} images unsynchronized"
exit_status = Status.CRITICAL
else:
status_text = f"All {n_up} images up"
if n_excluded_up:
status_text = f"{status_text}, {n_excluded_up} excluded but up"

if n_excluded_up and args.warn_excluded:
exit_status = Status.WARNING
else:
exit_status = Status.OK

print(f"{Status(exit_status).name} - {status_text} | {perf_data}")

if n_errors:
print('errors:')
for img in sorted(img_errors):
print(f"- {img} => {img_check_statuses[img]}")

if n_excluded and (args.show_excluded or args.warn_excluded):
print('excluded:')
for img in sorted(img_excluded):
print(f"- {img} => {img_check_statuses[img]}")

sys.exit(exit_status)
```

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.