Compare commits

...

24 commits

Author SHA1 Message Date
Benjamin Renard 057ddf2a0b Switch from Woodpecker CI to Forgejo Actions
All checks were successful
Run tests / test-precommit (push) Successful in 1m15s
2024-03-14 00:08:08 +01:00
Benjamin Renard 022c8fe7f6 Introduce pre-commit hooks 2024-03-14 00:02:33 +01:00
Benjamin Renard f3f8ae9f1a Fix NRPE command example
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-09-26 11:32:45 +02:00
Benjamin Renard 67aaf9852c Update git repo URL
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-09-26 11:25:07 +02:00
Benjamin Renard f1a1cc3193 Fix encoding of the TOUCH value (python3)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2022-08-29 09:44:36 +02:00
Benjamin Renard cdf35ecd5a CI: use brenard/aptly-publish image for publish-apt job
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2022-08-01 23:28:55 +02:00
Benjamin Renard f24ce7f266 CI: add release notes 2022-08-01 23:28:55 +02:00
Benjamin Renard 4dc2425387 Improve install doc
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-06-29 18:51:27 +02:00
Benjamin Renard d8a6267d7a Fix CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2022-05-01 03:15:17 +02:00
Benjamin Renard b1953a5b51 Add CI for testing and publishing (gitea version & debian package)
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-05-01 03:12:04 +02:00
Benjamin Renard d33df5c23d Fix pylint/flake8 warnings 2022-05-01 02:39:51 +02:00
Benjamin Renard a3d6c8cfb0 Add some info about dependencies and installation method 2021-03-23 18:05:39 +01:00
Björn Zettergren 2f6d97f761 fix byte-array / str
python3 returns byte-arrays instead of strings for ldap-data in
ContextCSNs, which makes the listcomprehension fail with a TypeError
when using the --serverID flag like so:

    CSN = [s for s in contextCSNs if sub in s]
TypeError: a bytes-like object is required, not 'str'

explicitly encoding 'sub' to byte-array, not sure if this is best way,
but seems to work well enough.
2021-03-23 17:48:34 +01:00
Benjamin Renard 5ce72df6dc Switch the script shebang to python3 2021-03-23 17:47:10 +01:00
Björn Zettergren 09722e94af linting long-lines
reorganized some low hanging fruit for fixing line-too-long reports from
pylint
2021-03-23 17:43:55 +01:00
Björn Zettergren 0b51046476 removed unused variables/constants
suspect these modules were added in initial commit after copying the
demo/example from python-ldap repository, possibly from
https://github.com/python-ldap/python-ldap/blob/master/Demo/initialize.py

These are seemingly never used, and pylint complains about them
2021-03-23 17:43:06 +01:00
Benjamin Renard 7153dfea86 Linting
Removed some redundant code that pylint-complaints about like `else` and
`elif` cases as suggest by Björn Zettergren <bjorn.zettergren@deltaprojects.com>
2021-03-23 17:40:04 +01:00
Benjamin Renard 36f9a09a34 Add --only-check-contextCSN parameter 2021-02-02 14:11:58 +01:00
Benjamin Renard 9e6de5077f Fix python3 compatibility 2021-02-02 11:38:29 +01:00
Benjamin Renard 3c39ff7488 Fix search scope on loading servers ContextCSN 2021-02-02 11:16:51 +01:00
Benjamin Renard d9ac73473d Update README and comments 2020-12-18 14:19:02 +01:00
Benjamin Renard d19d121843 Code cleaning 2020-12-18 11:24:09 +01:00
Benjamin Renard 99aa93a497 Fix touch mode when touch value is already in touch attibute and add --remove-touch-value parameter 2019-12-20 10:46:50 +01:00
Benjamin Renard 5b462ecc8a Add long output in nagios mode 2019-12-12 15:56:04 +01:00
17 changed files with 928 additions and 437 deletions

View file

@ -0,0 +1,89 @@
---
name: Build and publish Debian package
on: [create]
jobs:
build:
runs-on: docker
container:
image: docker.io/brenard/debian-python-deb:latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build Debian package
env:
MAINTAINER_NAME: ${{ vars.MAINTAINER_NAME }}
MAINTAINER_EMAIL: ${{ vars.MAINTAINER_EMAIL }}
DEBIAN_CODENAME: ${{ vars.DEBIAN_CODENAME }}
run: |
echo "${{ secrets.GPG_KEY }}"|base64 -d|gpg --import
./build.sh
mv check_syncrepl_extended dist/
- name: Upload Debian package files
uses: actions/upload-artifact@v3
with:
name: dist
path: |
dist/*.buildinfo
dist/*.changes
dist/*.deb
dist/*.dsc
dist/*.tar.gz
dist/release_notes.md
dist/check_syncrepl_extended
publish-forgejo:
runs-on: docker
container:
image: docker.io/brenard/debian-python-deb:latest
steps:
- name: Download Debian package files
uses: actions/download-artifact@v3
with:
name: dist
- name: Create the release
id: create-release
shell: bash
run: |
mkdir release
mv *.deb release/
mv check_syncrepl_extended release/
md5sum release/* > md5sum.txt
sha512sum release/* > sha512sum.txt
mv md5sum.txt sha512sum.txt release/
{
echo 'release_note<<EOF'
cat release_notes.md
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Publish release on Forgejo
uses: actions/forgejo-release@v1
with:
direction: upload
url: https://gitea.zionetrix.net
token: ${{ secrets.forgejo_token }}
release-dir: release
release-notes: ${{ steps.create-release.outputs.release_note }}
publish-aptly:
runs-on: docker
container:
image: docker.io/brenard/aptly-publish:latest
steps:
- name: "Download Debian package files"
uses: actions/download-artifact@v3
with:
name: dist
- name: "Publish Debian package on Aptly repository"
uses: https://gitea.zionetrix.net/bn8/aptly-publish@master
with:
api_url: ${{ vars.apt_api_url }}
api_username: ${{ vars.apt_api_username }}
api_password: ${{ secrets.apt_api_password }}
repo_name: ${{ vars.apt_repo_name }}
path: "./"
source_name: ${{ vars.apt_source_name }}

View file

@ -0,0 +1,21 @@
---
name: Run tests
on: [push]
jobs:
test-precommit:
runs-on: docker
container:
image: docker.io/brenard/python-pre-commit:latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install python dependencies
env:
DEBIAN_FRONTEND: noninteractive
run: |
apt-get -qq update
apt-get -qq -y install --no-install-recommends python3-ldap
- name: Run pre-commit
run: pre-commit run --all-files

1
.gitignore vendored
View file

@ -1 +1,2 @@
*~
dist/

64
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,64 @@
# Pre-commit hooks to run tests and ensure code is cleaned.
# See https://pre-commit.com for more information
---
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: ["--keep-percent-format", "--py37-plus"]
- repo: https://github.com/psf/black
rev: 23.11.0
hooks:
- id: black
args: ["--target-version", "py37", "--line-length", "100"]
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
args: ["--profile", "black", "--line-length", "100"]
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
args: ["--max-line-length=100"]
- repo: https://github.com/codespell-project/codespell
rev: v2.2.2
hooks:
- id: codespell
args:
- --ignore-words-list=exten
- --skip="./.*,*.csv,*.json,*.ini,*.subject,*.txt,*.html,*.log,*.conf"
- --quiet-level=2
- --ignore-regex=.*codespell-ignore$
# - --write-changes # Uncomment to write changes
exclude_types: [csv, json]
- repo: https://github.com/adrienverge/yamllint
rev: v1.32.0
hooks:
- id: yamllint
ignore: .github/
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
args: ["--print-width", "100"]
- repo: local
hooks:
- id: pylint
name: pylint
entry: pylint
language: system
types: [python]
require_serial: true
- repo: https://github.com/PyCQA/bandit
rev: 1.7.5
hooks:
- id: bandit
args: [--skip, "B101", --recursive]
minimum_pre_commit_version: 3.2.0

8
.pylintrc Normal file
View file

@ -0,0 +1,8 @@
[MESSAGES CONTROL]
disable=line-too-long,
missing-docstring,
invalid-name,
locally-disabled,
too-many-arguments,
too-many-branches,
redefined-outer-name,

135
README.md
View file

@ -1,42 +1,77 @@
Script to check LDAP syncrepl replication state between two servers
===================================================================
# Script to check LDAP syncrepl replication state between two servers
This script check LDAP syncrepl replication state between two servers.
One server is consider as provider and the other as consumer.
This script can check replication state with two method :
- by the fisrt, entryCSN of all entries of LDAP directory will be compare between two servers
- by the second, all values of all atributes of all entries will be compare between two servers.
In all case, contextCSN of servers will be compare and entries not present in consumer or in provider will be notice. You can decide to disable contextCSN verification by using argument *--no-check-contextCSN*.
- by the first, entryCSN of all entries of LDAP directory will be compare between two servers
- by the second, all values of all attributes of all entries will be compare between two servers.
This script is also able to *"touch"* LDAP object on provider to force synchronisation of this object. This mechanism consist to add *'%%TOUCH%%'* value to an attribute of this object and remove it just after. The
touched attribute is specify by parameter *--touch*. Of course, couple of DN and password provided, must have write right on this attribute.
In all case, contextCSN of servers will be compare and entries not present in consumer or in provider will be notice. You can decide to disable contextCSN verification by using argument _--no-check-contextCSN_.
If your prefer, you can use *--replace-touch* parameter to replace value of touched attribute instead of adding the touched value. Use-ful in case of single-value attribute.
This script is also able to _"touch"_ LDAP object on provider to force synchronisation of this object. This mechanism consist to add _'%%TOUCH%%'_ value to an attribute of this object and remove it just after. The
touched attribute is specify by parameter _--touch_. Of course, couple of DN and password provided, must have write right on this attribute.
To use this script as an Icinga (or Nagios) plugin, use *-n* argument
If your prefer, you can use _--replace-touch_ parameter to replace value of touched attribute instead of adding the touched value. Use-ful in case of single-value attribute.
Requirement
-----------
To use this script as an Icinga (or Nagios) plugin, use _-n_ argument
## Requirement
A single couple of DN and password able to connect to both server and without restriction to retrieve objects from servers.
Usage
-----
```
Usage: check_syncrepl_extended [options]
## Dependencies
Options:
--version show program's version number and exit
- python 3 (for python 2.7 compatibility, see python2.7 branch)
- python-ldap
## Installation
### If you plan to use it with NRPE
```
apt install -y python3-ldap git
git clone https://gitea.zionetrix.net/bn8/check_syncrepl_extended.git /usr/local/src/check_syncrepl_extended
mkdir -p /usr/local/lib/nagios/plugins
ln -s /usr/local/src/check_syncrepl_extended/check_syncrepl_extended /usr/local/lib/nagios/plugins/
cat << EOF > /etc/nagios/nrpe.d/ldap-syncrepl.cfg
command[check_syncrepl_extended]=/usr/local/lib/nagios/plugins/check_syncrepl_extended --nagios --attributes --provider ldaps://ldapmaster.foo --consumer ldaps://ldapslave.foo --basedn o=example -D uid=nagios,ou=sysaccounts,o=example -P secret
EOF
service nagios-nrpe-server reload
```
### Otherwise
```
apt install python3-ldap git
git clone https://gitea.zionetrix.net/bn8/check_syncrepl_extended.git /usr/local/src/check_syncrepl_extended
ln -s /usr/local/src/check_syncrepl_extended/check_syncrepl_extended /usr/local/bin/
```
## Usage
```
usage: check_syncrepl_extended [-h] [-v] [-p PROVIDER] [-c CONSUMER]
[-i SERVERID] [-T] [-D DN] [-P PWD] [-b BASEDN]
[-f FILTERSTR] [-d] [-n] [-q]
[--no-check-certificate]
[--no-check-contextCSN] [-a]
[--exclude-attributes EXCL_ATTRS]
[--touch TOUCH] [--replace-touch]
[--remove-touch-value] [--page-size PAGE_SIZE]
Script to check LDAP syncrepl replication state between two servers.
optional arguments:
-h, --help show this help message and exit
-p PROVIDER, --provider=PROVIDER
LDAP provider URI (example :
-v, --version show program's version number and exit
-p PROVIDER, --provider PROVIDER
LDAP provider URI (example:
ldaps://ldapmaster.foo:636)
-c CONSUMER, --consumer=CONSUMER
LDAP consumer URI (example :
ldaps://ldapslave.foo:636)
-i SERVERID, --serverID=SERVERID
-c CONSUMER, --consumer CONSUMER
LDAP consumer URI (example: ldaps://ldapslave.foo:636)
-i SERVERID, --serverID SERVERID
Compare contextCSN of a specific master. Useful in
MultiMaster setups where each master has a unique ID
and a contextCSN for each replicated master exists. A
@ -44,47 +79,47 @@ Options:
(limited to 3 hex digits, example: '12' compares the
contextCSN matching '#00C#')
-T, --starttls Start TLS on LDAP provider/consumers connections
-D DN, --dn=DN LDAP bind DN (example :
-D DN, --dn DN LDAP bind DN (example:
uid=nagios,ou=sysaccounts,o=example
-P PWD, --pwd=PWD LDAP bind password
-b BASEDN, --basedn=BASEDN
LDAP base DN (example : o=example)
-f FILTER, --filter=FILTER
LDAP filter (default : (objectClass=*))
-P PWD, --pwd PWD LDAP bind password
-b BASEDN, --basedn BASEDN
LDAP base DN (example: o=example)
-f FILTERSTR, --filter FILTERSTR
LDAP filter (default: (objectClass=*))
-d, --debug Debug mode
-n, --nagios Nagios check plugin mode
-q, --quiet Quiet mode
--no-check-certificate
Don't check the server certificate (Default : False)
Don't check the server certificate (Default: False)
--no-check-contextCSN
Don't check servers contextCSN (Default : False)
-a, --attributes Check attributes values (Default : check only
entryCSN)
--exclude-attributes=EXCL_ATTRS
Don't check this attribut (only in attribute check
Don't check servers contextCSN (Default: False)
-a, --attributes Check attributes values (Default: check only entryCSN)
--exclude-attributes EXCL_ATTRS
Don't check this attribute (only in attribute check
mode)
--touch=TOUCH Touch attribute giving in parameter to force resync a
this LDAP object from provider. A value '%%TOUCH%%'
will be add to this attribute and remove after. The
user use to connect to the LDAP directory must have
write permission on this attribute on each object.
--touch TOUCH Touch attribute giving in parameter to force resync a
this LDAP object from provider. A value '%TOUCH%' will
be add to this attribute and remove after. The user
use to connect to the LDAP directory must have write
permission on this attribute on each object.
--replace-touch In touch mode, replace value instead of adding.
--page-size=PAGE_SIZE
Page size : if defined, paging control using LDAP v3
--remove-touch-value In touch mode, remove touch value if present.
--page-size PAGE_SIZE
Page size: if defined, paging control using LDAP v3
extended control will be enabled.
Author: Benjamin Renard <brenard@easter-eggs.com>, Source:
https://gitea.zionetrix.net/bn8/check_syncrepl_extended
```
Copyright
---------
## Copyright
Copyright (c) 2017 Benjamin Renard
License
-------
## License
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

62
build.sh Executable file
View file

@ -0,0 +1,62 @@
#!/bin/bash
QUIET_ARG=""
[ "$1" == "--quiet" ] && QUIET_ARG="--quiet"
# Enter source directory
cd $( dirname $0 )
echo "Clean previous build..."
rm -fr dist
echo "Detect version using git describe..."
VERSION="$( git describe --tags|sed 's/^[^0-9]*//' )"
echo "Create building environemt..."
BDIR=dist/check-syncrepl-extended-$VERSION
mkdir -p $BDIR
[ -z "$QUIET_ARG" ] && RSYNC_ARG="-v" || RSYNC_ARG=""
rsync -a $RSYNC_ARG debian/ $BDIR/debian/
cp check_syncrepl_extended $BDIR/
echo "Set VERSION=$VERSION in gitdch using sed..."
sed -i "s/^VERSION *=.*$/VERSION = '$VERSION'/" $BDIR/check_syncrepl_extended
if [ -z "$DEBIAN_CODENAME" ]
then
echo "Retrieve debian codename using lsb_release..."
DEBIAN_CODENAME=$( lsb_release -c -s )
else
echo "Use debian codename from environment ($DEBIAN_CODENAME)"
fi
echo "Generate debian changelog using gitdch..."
GITDCH_ARGS=('--verbose')
[ -n "$QUIET_ARG" ] && GITDCH_ARGS=('--warning')
if [ -n "$MAINTAINER_NAME" ]
then
echo "Use maintainer name from environment ($MAINTAINER_NAME)"
GITDCH_ARGS+=("--maintainer-name" "${MAINTAINER_NAME}")
fi
if [ -n "$MAINTAINER_EMAIL" ]
then
echo "Use maintainer email from environment ($MAINTAINER_EMAIL)"
GITDCH_ARGS+=("--maintainer-email" "$MAINTAINER_EMAIL")
fi
gitdch \
--package-name check-syncrepl-extended \
--version "${VERSION}" \
--code-name $DEBIAN_CODENAME \
--output $BDIR/debian/changelog \
--release-notes dist/release_notes.md \
"${GITDCH_ARGS[@]}"
if [ -n "$MAINTAINER_NAME" -a -n "$MAINTAINER_EMAIL" ]
then
echo "Set Maintainer field in debian control file ($MAINTAINER_NAME <$MAINTAINER_EMAIL>)..."
sed -i "s/^Maintainer: .*$/Maintainer: $MAINTAINER_NAME <$MAINTAINER_EMAIL>/" $BDIR/debian/control
fi
echo "Build debian package..."
cd $BDIR
dpkg-buildpackage

View file

@ -1,13 +1,13 @@
#!/usr/bin/python
#!/usr/bin/env python3
#
# Script to check LDAP syncrepl replication state between two servers.
# One server is consider as provider and the other as consumer.
#
# This script can check replication state with two method :
# - by the fisrt, entryCSN of all entries of LDAP directory will be
# - by the first, entryCSN of all entries of LDAP directory will be
# compare between two servers
# - by the second, all values of all atributes of all entries will
# - by the second, all values of all attributes of all entries will
# be compare between two servers.
#
# In all case, contextCSN of servers will be compare and entries not
@ -26,454 +26,608 @@
#
# This script could be use as Nagios plugin (-n argument)
#
# Requirement :
# A single couple of DN and password able to connect to both server
# Requirement:
# A single couple of DN and password able to connect to both server
# and without restriction to retrieve objects from servers.
#
# Author : Benjamin Renard <brenard@easter-eggs.com>
# Date : Mon, 10 Dec 2012 18:04:24 +0100
# Source : http://git.zionetrix.net/check_syncrepl_extended
# Author: Benjamin Renard <brenard@easter-eggs.com>
# Source: https://gitea.zionetrix.net/bn8/check_syncrepl_extended
#
import ldap
from ldap.controls import SimplePagedResultsControl
import ldap.modlist as modlist
import argparse
import getpass
import logging
import sys
import getpass
from optparse import OptionParser
import ldap
from ldap import LDAPError # pylint: disable=no-name-in-module
from ldap import modlist
from ldap.controls import SimplePagedResultsControl
TOUCH_VALUE='%%TOUCH%%'
VERSION = "0.0"
TOUCH_VALUE = b"%%TOUCH%%"
parser = OptionParser(version="%prog version 1.0\n\nDate : Mon, 10 Dec 2012 18:04:24 +0100\nAuthor : Benjamin Renard <brenard@easter-eggs.com>\nSource : http://git.zionetrix.net/check_syncrepl_extended")
parser = argparse.ArgumentParser(
description=("Script to check LDAP syncrepl replication state between two servers."),
epilog=(
"Author: Benjamin Renard <brenard@easter-eggs.com>, "
f"Version: {VERSION}, "
"Source: https://gitea.zionetrix.net/bn8/check_syncrepl_extended"
),
)
parser.add_option( "-p", "--provider",
dest="provider",
action="store",
type='string',
help="LDAP provider URI (example : ldaps://ldapmaster.foo:636)")
parser.add_argument(
"-p",
"--provider",
dest="provider",
action="store",
type=str,
help="LDAP provider URI (example: ldaps://ldapmaster.foo:636)",
)
parser.add_option( "-c", "--consumer",
dest="consumer",
action="store",
type='string',
help="LDAP consumer URI (example : ldaps://ldapslave.foo:636)")
parser.add_argument(
"-c",
"--consumer",
dest="consumer",
action="store",
type=str,
help="LDAP consumer URI (example: ldaps://ldapslave.foo:636)",
)
parser.add_option( "-i", "--serverID",
dest="serverid",
action="store",
type='int',
help="Compare contextCSN of a specific master. Useful in MultiMaster setups where each master has a unique ID and a contextCSN for each replicated master exists. A valid serverID is a integer value from 0 to 4095 (limited to 3 hex digits, example: '12' compares the contextCSN matching '#00C#')",
default=False)
parser.add_argument(
"-i",
"--serverID",
dest="serverid",
action="store",
type=int,
help=(
"Compare contextCSN of a specific master. Useful in MultiMaster "
"setups where each master has a unique ID and a contextCSN for "
"each replicated master exists. A valid serverID is a integer "
"value from 0 to 4095 (limited to 3 hex digits, example: '12' "
"compares the contextCSN matching '#00C#')"
),
default=False,
)
parser.add_option( "-T", "--starttls",
dest="starttls",
action="store_true",
help="Start TLS on LDAP provider/consumers connections",
default=False)
parser.add_argument(
"-T",
"--starttls",
dest="starttls",
action="store_true",
help="Start TLS on LDAP provider/consumers connections",
default=False,
)
parser.add_option( "-D", "--dn",
dest="dn",
action="store",
type='string',
help="LDAP bind DN (example : uid=nagios,ou=sysaccounts,o=example")
parser.add_argument(
"-D",
"--dn",
dest="dn",
action="store",
type=str,
help="LDAP bind DN (example: uid=nagios,ou=sysaccounts,o=example",
)
parser.add_option( "-P", "--pwd",
dest="pwd",
action="store",
type='string',
help="LDAP bind password",
default=None)
parser.add_argument(
"-P", "--pwd", dest="pwd", action="store", type=str, help="LDAP bind password", default=None
)
parser.add_option( "-b", "--basedn",
dest="basedn",
action="store",
type='string',
help="LDAP base DN (example : o=example)")
parser.add_argument(
"-b",
"--basedn",
dest="basedn",
action="store",
type=str,
help="LDAP base DN (example: o=example)",
)
parser.add_option( "-f", "--filter",
dest="filter",
action="store",
type='string',
help="LDAP filter (default : (objectClass=*))",
default='(objectClass=*)')
parser.add_argument(
"-f",
"--filter",
dest="filterstr",
action="store",
type=str,
help="LDAP filter (default: (objectClass=*))",
default="(objectClass=*)",
)
parser.add_option( "-d", "--debug",
dest="debug",
action="store_true",
help="Debug mode",
default=False)
parser.add_argument(
"-d", "--debug", dest="debug", action="store_true", help="Debug mode", default=False
)
parser.add_option( "-n", "--nagios",
dest="nagios",
action="store_true",
help="Nagios check plugin mode",
default=False)
parser.add_argument(
"-n",
"--nagios",
dest="nagios",
action="store_true",
help="Nagios check plugin mode",
default=False,
)
parser.add_option( "-q", "--quiet",
dest="quiet",
action="store_true",
help="Quiet mode",
default=False)
parser.add_argument(
"-q", "--quiet", dest="quiet", action="store_true", help="Quiet mode", default=False
)
parser.add_option( "--no-check-certificate",
dest="nocheckcert",
action="store_true",
help="Don't check the server certificate (Default : False)",
default=False)
parser.add_argument(
"--no-check-certificate",
dest="nocheckcert",
action="store_true",
help="Don't check the server certificate (Default: False)",
default=False,
)
parser.add_option( "--no-check-contextCSN",
dest="nocheckcontextcsn",
action="store_true",
help="Don't check servers contextCSN (Default : False)",
default=False)
parser.add_argument(
"--no-check-contextCSN",
dest="nocheckcontextcsn",
action="store_true",
help="Don't check servers contextCSN (Default: False)",
default=False,
)
parser.add_option( "-a", "--attributes",
dest="attrs",
action="store_true",
help="Check attributes values (Default : check only entryCSN)",
default=False)
parser.add_argument(
"--only-check-contextCSN",
dest="onlycheckcontextcsn",
action="store_true",
help=("Only check servers root contextCSN (objects check disabled, default : False)"),
default=False,
)
parser.add_option( "--exclude-attributes",
dest="excl_attrs",
action="store",
type='string',
help="Don't check this attribut (only in attribute check mode)",
default=None)
parser.add_argument(
"-a",
"--attributes",
dest="attrs",
action="store_true",
help="Check attributes values (Default: check only entryCSN)",
default=False,
)
parser.add_option( "--touch",
dest="touch",
action="store",
type='string',
help="Touch attribute giving in parameter to force resync a this LDAP object from provider. A value '%s' will be add to this attribute and remove after. The user use to connect to the LDAP directory must have write permission on this attribute on each object." % TOUCH_VALUE,
default=None)
parser.add_argument(
"--exclude-attributes",
dest="excl_attrs",
action="store",
type=str,
help="Don't check this attribute (only in attribute check mode)",
default=None,
)
parser.add_option( "--replace-touch",
dest="replacetouch",
action="store_true",
help="In touch mode, replace value instead of adding.",
default=False)
parser.add_argument(
"--touch",
dest="touch",
action="store",
type=str,
help=(
"Touch attribute giving in parameter to force resync a this LDAP "
f'object from provider. A value "{TOUCH_VALUE.decode()}" will be '
"add to this attribute and remove after. The user use to connect "
"to the LDAP directory must have write permission on this "
"attribute on each object."
),
default=None,
)
parser.add_option( "--page-size",
dest="page_size",
action="store",
type='int',
help="Page size : if defined, paging control using LDAP v3 extended control will be enabled.",
default=None)
parser.add_argument(
"--replace-touch",
dest="replacetouch",
action="store_true",
help="In touch mode, replace value instead of adding.",
default=False,
)
(options, args) = parser.parse_args()
parser.add_argument(
"--remove-touch-value",
dest="removetouchvalue",
action="store_true",
help="In touch mode, remove touch value if present.",
default=False,
)
parser.add_argument(
"--page-size",
dest="page_size",
action="store",
type=int,
help=("Page size: if defined, paging control using LDAP v3 extended control will be enabled."),
default=None,
)
options = parser.parse_args()
if options.nocheckcontextcsn and options.onlycheckcontextcsn:
parser.error(
"You can't use both --no-check-contextCSN and "
"--only-check-contextCSN parameters and the same time"
)
if options.nagios:
sys.exit(3)
sys.exit(1)
if not options.provider or not options.consumer:
print "You must provide provider and customer URI"
if options.nagios:
sys.exit(3)
sys.exit(1)
parser.error("You must provide provider and customer URI")
if options.nagios:
sys.exit(3)
sys.exit(1)
if not options.basedn:
print "You must provide base DN of connection to LDAP servers"
if options.nagios:
sys.exit(3)
sys.exit(1)
parser.error("You must provide base DN of connection to LDAP servers")
if options.nagios:
sys.exit(3)
sys.exit(1)
if not 0 <= options.serverid <= 4095:
print "ServerID should be a integer value from 0 to 4095 (limited to 3 hexadecimal digits)."
if options.nagios:
sys.exit(3)
sys.exit(1)
parser.error(
"ServerID should be a integer value from 0 to 4095 (limited to 3 hexadecimal digits)."
)
if options.nagios:
sys.exit(3)
sys.exit(1)
if options.touch and not options.attrs:
logging.info('Force option attrs on touch mode')
options.attrs=True
logging.info("Force option attrs on touch mode")
options.attrs = True
if options.dn and options.pwd is None:
options.pwd=getpass.getpass()
options.pwd = getpass.getpass()
excl_attrs=[]
excl_attrs = []
if options.excl_attrs:
for ex in options.excl_attrs.split(','):
excl_attrs.append(ex.strip())
for ex in options.excl_attrs.split(","):
excl_attrs.append(ex.strip())
FORMAT="%(asctime)s - %(levelname)s : %(message)s"
FORMAT = "%(asctime)s - %(levelname)s: %(message)s"
if options.debug:
logging.basicConfig(level=logging.DEBUG,format=FORMAT)
ldap.set_option(ldap.OPT_DEBUG_LEVEL,0)
ldapmodule_trace_level = 1
ldapmodule_trace_file = sys.stderr
logging.basicConfig(level=logging.DEBUG, format=FORMAT)
ldap.set_option(ldap.OPT_DEBUG_LEVEL, 0) # pylint: disable=no-member
elif options.nagios:
logging.basicConfig(level=logging.ERROR,format=FORMAT)
logging.basicConfig(level=logging.ERROR, format=FORMAT)
elif options.quiet:
logging.basicConfig(level=logging.WARNING,format=FORMAT)
logging.basicConfig(level=logging.WARNING, format=FORMAT)
else:
logging.basicConfig(level=logging.INFO,format=FORMAT)
logging.basicConfig(level=logging.INFO, format=FORMAT)
class LdapServer(object):
uri = ""
dn = ""
pwd = ""
start_tls = False
class LdapServer:
uri = None
dn = None
pwd = None
start_tls = False
con = 0
con = 0
def __init__(self,uri,dn,pwd, start_tls=False, page_size=None):
self.uri = uri
self.dn = dn
self.pwd = pwd
self.start_tls = start_tls
self.page_size = page_size
def __init__(self, uri, dn, pwd, start_tls=False, page_size=None):
self.uri = uri
self.dn = dn
self.pwd = pwd
self.start_tls = start_tls
self.page_size = page_size
def connect(self):
if self.con == 0:
try:
con = ldap.initialize(self.uri)
con.protocol_version = ldap.VERSION3
if self.start_tls:
con.start_tls_s()
if self.dn:
con.simple_bind_s(self.dn,self.pwd)
self.con = con
return True
except ldap.LDAPError, e:
logging.error("LDAP Error : %s" % e)
return
def connect(self):
if self.con == 0:
try:
con = ldap.initialize(self.uri)
# pylint: disable=no-member
con.protocol_version = ldap.VERSION3
if self.start_tls:
con.start_tls_s()
if self.dn:
con.simple_bind_s(self.dn, self.pwd)
self.con = con
except LDAPError:
logging.error("LDAP Error", exc_info=True)
return False
return True
def getContextCSN(self,basedn=False,serverid=False):
if not basedn:
basedn=self.dn
data=self.search(basedn,'(objectclass=*)',['contextCSN'])
if len(data)>0:
contextCSNs=data[0][0][1]['contextCSN']
logging.debug('Found contextCSNs %s' % contextCSNs)
if serverid is False:
return contextCSNs[0]
else:
csnid=str(format(serverid, 'X')).zfill(3)
sub='#%s#' % csnid
CSN=[s for s in contextCSNs if sub in s]
if not CSN:
logging.error("No contextCSN matching with ServerID %s (=%s) could be found." % (serverid,sub))
return False
else:
return CSN[0]
else:
return False
def getContextCSN(self, basedn=False, serverid=False):
if not basedn:
basedn = self.dn
data = self.search(basedn, "(objectclass=*)", attrs=["contextCSN"], scope="base")
if data:
contextCSNs = data[0][0][1]["contextCSN"]
logging.debug("Found contextCSNs %s", contextCSNs)
if serverid is False:
return contextCSNs[0]
csnid = str(format(serverid, "X")).zfill(3)
sub = str.encode(f"#{csnid}#", encoding="ascii", errors="replace")
CSN = [s for s in contextCSNs if sub in s]
if not CSN:
logging.error(
"No contextCSN matching with ServerID %s (=%s) could be found.",
serverid,
sub,
)
return False
return CSN[0]
return False
def search(self,basedn,filter,attrs):
if self.page_size:
return self.paged_search(basedn,filter,attrs)
res_id = self.con.search(basedn,ldap.SCOPE_SUBTREE,filter,attrs)
ret = []
while 1:
res_type, res_data = self.con.result(res_id,0)
if res_data == []:
break
else:
if res_type == ldap.RES_SEARCH_ENTRY:
ret.append(res_data)
return ret
@staticmethod
def get_scope(scope):
if scope == "base":
return ldap.SCOPE_BASE # pylint: disable=no-member
if scope == "one":
return ldap.SCOPE_ONELEVEL # pylint: disable=no-member
if scope == "sub":
return ldap.SCOPE_SUBTREE # pylint: disable=no-member
raise Exception(f'Unknown LDAP scope "{scope}"') # pylint: disable=broad-exception-raised
def paged_search(self,basedn,filter,attrs):
ret = []
page = 0
pg_ctrl = SimplePagedResultsControl(True, self.page_size, '')
pg_oid = SimplePagedResultsControl.controlType
while page == 0 or pg_ctrl.cookie:
page += 1
logging.debug('Page search : loading page %d' % page)
res_id = self.con.search_ext(basedn,ldap.SCOPE_SUBTREE,filter,attrs,serverctrls=[pg_ctrl])
res_type, res_data, res_id, serverctrls = self.con.result3(res_id)
for serverctrl in serverctrls:
if serverctrl.controlType == pg_oid:
pg_ctrl.cookie = serverctrl.cookie
for item in res_data:
ret.append([item])
return ret
def search(self, basedn, filterstr, attrs=None, scope=None):
if self.page_size:
return self.paged_search(basedn, filterstr, attrs=attrs, scope=scope)
res_id = self.con.search(
basedn, self.get_scope(scope if scope else "sub"), filterstr, attrs if attrs else []
)
ret = []
while 1:
res_type, res_data = self.con.result(res_id, 0)
if res_data == []:
break
if res_type == ldap.RES_SEARCH_ENTRY: # pylint: disable=no-member
ret.append(res_data)
return ret
def update_object(self,dn,old,new):
ldif = modlist.modifyModlist(old,new)
if ldif == []:
return True
try:
logging.debug('Update object %s : %s' % (dn,ldif))
self.con.modify_s(dn,ldif)
return True
except ldap.LDAPError, e:
logging.error('Error updating object %s : %s' % (dn,e))
return False
def paged_search(self, basedn, filterstr, attrs=None, scope=None):
ret = []
page = 0
pg_ctrl = SimplePagedResultsControl(True, self.page_size, "")
while page == 0 or pg_ctrl.cookie:
page += 1
logging.debug("Page search: loading page %d", page)
res_id = self.con.search_ext(
basedn,
self.get_scope(scope if scope else "sub"),
filterstr,
attrs if attrs else [],
serverctrls=[pg_ctrl],
)
# pylint: disable=unused-variable
res_type, res_data, res_id, serverctrls = self.con.result3(res_id)
for serverctrl in serverctrls:
if serverctrl.controlType == SimplePagedResultsControl.controlType:
pg_ctrl.cookie = serverctrl.cookie
break
for item in res_data:
ret.append([item])
return ret
def get_attr(self,obj,attr):
if attr in obj[0][1]:
return obj[0][1][attr]
return []
def update_object(self, dn, old, new):
ldif = modlist.modifyModlist(old, new)
if not ldif:
return True
try:
logging.debug("Update object %s: %s", dn, ldif)
self.con.modify_s(dn, ldif)
return True
except LDAPError:
logging.error("Error updating object %s", dn, exc_info=True)
return False
@staticmethod
def get_attr(obj, attr):
if attr in obj[0][1]:
return obj[0][1][attr]
return []
def touch_object(self, dn, attr, orig_value):
old = {}
if orig_value:
old[attr] = orig_value
new = {}
if options.replacetouch:
if not orig_value or TOUCH_VALUE not in orig_value:
new[attr] = [TOUCH_VALUE]
else:
new[attr] = list(orig_value)
if orig_value or TOUCH_VALUE in orig_value:
new[attr].remove(TOUCH_VALUE)
else:
new[attr].append(TOUCH_VALUE)
try:
logging.info('Touch object "%s" on attribute "%s": %s => %s', dn, attr, old, new)
if self.update_object(dn, old, new):
logging.info('Restore original value of attribute "%s" of object "%s"', attr, dn)
if options.removetouchvalue and TOUCH_VALUE in old[attr]:
old[attr].remove(TOUCH_VALUE)
self.update_object(dn=dn, old=new, new=old)
return True
except LDAPError:
logging.error('Error touching object "%s"', dn, exc_info=True)
return False
def touch_object(self,dn,attr,orig_value):
if options.replacetouch:
new_value=[TOUCH_VALUE]
else:
new_value=list(orig_value)
new_value.append(TOUCH_VALUE)
try:
logging.info('Add value "%s" to attribute %s of object %s' % (TOUCH_VALUE,attr,dn))
if self.update_object(dn,{attr: orig_value}, {attr: new_value}):
logging.info('Remove value "%s" to attribute %s of object %s' % (TOUCH_VALUE,attr,dn))
self.update_object(dn,{attr: new_value}, {attr: orig_value})
return True
except ldap.LDAPError, e:
logging.error('Error touching object %s : %s' % (dn,e))
return False
if options.nocheckcert:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,ldap.OPT_X_TLS_NEVER)
# pylint: disable=no-member
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
servers=[options.provider,options.consumer]
servers = [options.provider, options.consumer]
LdapServers={}
LdapObjects={}
LdapServersCSN={}
LdapServers = {}
LdapObjects = {}
LdapServersCSN = {}
for srv in servers:
logging.info('Connect to %s' % srv)
LdapServers[srv]=LdapServer(srv, options.dn, options.pwd, options.starttls, page_size=options.page_size)
logging.info("Connect to %s", srv)
LdapServers[srv] = LdapServer(
srv, options.dn, options.pwd, options.starttls, page_size=options.page_size
)
if not LdapServers[srv].connect():
if options.nagios:
print "UNKWNON - Failed to connect to %s" % srv
sys.exit(3)
else:
sys.exit(1)
if not LdapServers[srv].connect():
if options.nagios:
print(f"UNKWNON - Failed to connect to {srv}")
sys.exit(3)
else:
sys.exit(1)
if not options.nocheckcontextcsn:
LdapServersCSN[srv]=LdapServers[srv].getContextCSN(options.basedn,options.serverid)
logging.info('ContextCSN of %s : %s' % (srv, LdapServersCSN[srv]))
if not options.nocheckcontextcsn:
LdapServersCSN[srv] = LdapServers[srv].getContextCSN(options.basedn, options.serverid)
logging.info("ContextCSN of %s: %s", srv, LdapServersCSN[srv])
logging.info('List objects from %s' % srv)
LdapObjects[srv]={}
if not options.onlycheckcontextcsn:
logging.info("List objects from %s", srv)
LdapObjects[srv] = {}
if options.attrs:
for obj in LdapServers[srv].search(options.basedn,options.filter,[]):
logging.debug('Found on %s : %s' % (srv,obj[0][0]))
LdapObjects[srv][obj[0][0]]=obj[0][1]
else:
for obj in LdapServers[srv].search(options.basedn,options.filter,['entryCSN']):
logging.debug('Found on %s : %s / %s' % (srv,obj[0][0],obj[0][1]['entryCSN'][0]))
LdapObjects[srv][obj[0][0]]=obj[0][1]['entryCSN'][0]
if options.attrs:
for obj in LdapServers[srv].search(options.basedn, options.filterstr, []):
logging.debug("Found on %s: %s", srv, obj[0][0])
LdapObjects[srv][obj[0][0]] = obj[0][1]
else:
for obj in LdapServers[srv].search(options.basedn, options.filterstr, ["entryCSN"]):
logging.debug("Found on %s: %s / %s", srv, obj[0][0], obj[0][1]["entryCSN"][0])
LdapObjects[srv][obj[0][0]] = obj[0][1]["entryCSN"][0]
logging.info('%s objects founds' % len(LdapObjects[srv]))
logging.info("%s objects founds", len(LdapObjects[srv]))
not_found={}
not_sync={}
if not options.onlycheckcontextcsn:
not_found = {}
not_sync = {}
for srv in servers:
not_found[srv]=[]
not_sync[srv]=[]
for srv in servers:
not_found[srv] = []
not_sync[srv] = []
if options.attrs:
logging.info("Check if objects a are synchronized (by comparing attributes's values)")
else:
logging.info('Check if objets are synchronized (by comparing entryCSN)')
for obj in LdapObjects[options.provider]:
logging.debug('Check obj %s' % (obj))
for srv in LdapObjects:
if srv == options.provider:
continue
if obj in LdapObjects[srv]:
touch=False
if LdapObjects[options.provider][obj] != LdapObjects[srv][obj]:
if options.attrs:
attrs_list=[]
for attr in LdapObjects[options.provider][obj]:
if attr in excl_attrs:
continue
if attr not in LdapObjects[srv][obj]:
attrs_list.append(attr)
logging.debug("Obj %s not synchronized : %s not present on %s" % (obj,','.join(attrs_list),srv))
touch=True
else:
LdapObjects[srv][obj][attr].sort()
LdapObjects[options.provider][obj][attr].sort()
if LdapObjects[srv][obj][attr]!=LdapObjects[options.provider][obj][attr]:
attrs_list.append(attr)
logging.debug("Obj %s not synchronized : %s not same value(s)" % (obj,','.join(attrs_list)))
touch=True
if len(attrs_list)>0:
not_sync[srv].append("%s (%s)" % (obj,','.join(attrs_list)))
else:
logging.debug("Obj %s not synchronized : %s <-> %s" % (obj,LdapObjects[options.provider][obj],LdapObjects[srv][obj]))
not_sync[srv].append(obj)
if touch and options.touch:
orig_value=[]
if options.touch in LdapObjects[options.provider][obj]:
orig_value=LdapObjects[options.provider][obj][options.touch]
LdapServers[options.provider].touch_object(obj,options.touch,orig_value)
else:
logging.debug('Obj %s : not found on %s' % (obj,srv))
not_found[srv].append(obj)
if options.touch:
orig_value=[]
if options.touch in LdapObjects[options.provider][obj]:
orig_value=LdapObjects[options.provider][obj][options.touch]
LdapServers[options.provider].touch_object(obj,options.touch,orig_value)
if options.attrs:
logging.info("Check if objects a are synchronized (by comparing attributes's values)")
else:
logging.info("Check if objects are synchronized (by comparing entryCSN)")
for obj in LdapObjects[options.provider]:
logging.debug("Check obj %s", obj)
for srv_name, srv in LdapObjects.items():
if srv_name == options.provider:
continue
if obj in srv:
touch = False
if LdapObjects[options.provider][obj] != srv[obj]:
if options.attrs:
attrs_list = []
for attr in LdapObjects[options.provider][obj]:
if attr in excl_attrs:
continue
if attr not in srv[obj]:
attrs_list.append(attr)
logging.debug(
"Obj %s not synchronized: %s not present on %s",
obj,
",".join(attrs_list),
srv_name,
)
touch = True
else:
srv[obj][attr].sort()
LdapObjects[options.provider][obj][attr].sort()
if srv[obj][attr] != LdapObjects[options.provider][obj][attr]:
attrs_list.append(attr)
logging.debug(
"Obj %s not synchronized: %s not same value(s)",
obj,
",".join(attrs_list),
)
touch = True
if attrs_list:
not_sync[srv_name].append(f'{obj} ({",".join(attrs_list)})')
else:
logging.debug(
"Obj %s not synchronized: %s <-> %s",
obj,
LdapObjects[options.provider][obj],
srv[obj],
)
not_sync[srv_name].append(obj)
if touch and options.touch:
orig_value = []
if options.touch in LdapObjects[options.provider][obj]:
orig_value = LdapObjects[options.provider][obj][options.touch]
LdapServers[options.provider].touch_object(obj, options.touch, orig_value)
else:
logging.debug("Obj %s: not found on %s", obj, srv_name)
not_found[srv_name].append(obj)
if options.touch:
orig_value = []
if options.touch in LdapObjects[options.provider][obj]:
orig_value = LdapObjects[options.provider][obj][options.touch]
LdapServers[options.provider].touch_object(obj, options.touch, orig_value)
for obj in LdapObjects[options.consumer]:
logging.debug('Check obj %s of consumer' % obj)
if obj not in LdapObjects[options.provider]:
logging.debug('Obj %s : not found on provider' % obj)
not_found[options.provider].append(obj)
for obj in LdapObjects[options.consumer]:
logging.debug("Check obj %s of consumer", obj)
if obj not in LdapObjects[options.provider]:
logging.debug("Obj %s: not found on provider", obj)
not_found[options.provider].append(obj)
if options.nagios:
errors=[]
errors = []
long_output = []
if not options.nocheckcontextcsn:
if not LdapServersCSN[options.provider]:
errors.append('ContextCSN of LDAP server provider could not be found')
else:
for srv in LdapServersCSN:
if srv==options.provider:
continue
if not LdapServersCSN[srv]:
errors.append('ContextCSN of %s not found' % srv)
elif LdapServersCSN[srv]!=LdapServersCSN[options.provider]:
errors.append('ContextCSN of %s not the same of provider' % srv)
if not options.nocheckcontextcsn:
if not LdapServersCSN[options.provider]:
errors.append("ContextCSN of LDAP server provider could not be found")
else:
long_output.append(
f"ContextCSN on LDAP server provider = {LdapServersCSN[options.provider]}"
)
for srv_name, srv_csn in LdapServersCSN.items():
if srv_name == options.provider:
continue
if not srv_csn:
errors.append(f"ContextCSN of {srv_name} not found")
elif srv_csn != LdapServersCSN[options.provider]:
errors.append(f"ContextCSN of {srv_name} not the same of provider")
long_output.append(f"ContextCSN on LDAP server {srv_name} = {srv_csn}")
if len(not_found[options.consumer])>0:
errors.append("%s not found object(s) on consumer" % len(not_found[options.consumer]))
if len(not_found[options.provider])>0:
errors.append("%s not found object(s) on provider" % len(not_found[options.provider]))
if len(not_sync[options.consumer])>0:
errors.append("%s not synchronized object(s) on consumer" % len(not_sync[options.consumer]))
if len(errors)>0:
print "CRITICAL : "+', '.join(errors)
sys.exit(2)
else:
print 'OK : consumer and provider are synchronized'
sys.exit(0)
if not options.onlycheckcontextcsn:
if not_found[options.consumer]:
errors.append(f"{len(not_found[options.consumer])} not found object(s) on consumer")
long_output.append(f"Object(s) not found on server {options.consumer} (consumer):")
for obj in not_found[options.consumer]:
long_output.append(f" - {obj}")
if not_found[options.provider]:
errors.append(f"{len(not_found[options.provider])} not found object(s) on provider")
long_output.append(f"Object(s) not found on server {options.provider} (provider):")
for obj in not_found[options.provider]:
long_output.append(f" - {obj}")
if not_sync[options.consumer]:
errors.append(
f"{len(not_sync[options.consumer])} not synchronized object(s) on consumer"
)
long_output.append(
f"Object(s) not synchronized on server {options.consumer} (consumer):"
)
for obj in not_sync[options.consumer]:
long_output.append(f" - {obj}")
if errors:
print(f'CRITICAL: {", ".join(errors)}')
print("\n\n")
print("\n".join(long_output))
sys.exit(2)
else:
print("OK: consumer and provider are synchronized")
sys.exit(0)
else:
noerror=True
for srv in servers:
if not options.nocheckcontextcsn:
if not LdapServersCSN[options.provider]:
logging.warning('ContextCSN of LDAP server provider could not be found')
noerror=False
else:
for srv in LdapServersCSN:
if srv==options.provider:
continue
if not LdapServersCSN[srv]:
logging.warning('ContextCSN of %s not found' % srv)
noerror=False
elif LdapServersCSN[srv]!=LdapServersCSN[options.provider]:
logging.warning('ContextCSN of %s not the same of provider' % srv)
noerror=False
noerror = True
for srv in servers:
if not options.nocheckcontextcsn:
if not LdapServersCSN[options.provider]:
logging.warning("ContextCSN of LDAP server provider could not be found")
noerror = False
else:
for srv_name, srv_csn in LdapServersCSN.items():
if srv_name == options.provider:
continue
if not srv_csn:
logging.warning("ContextCSN of %s not found", srv_name)
noerror = False
elif srv_csn != LdapServersCSN[options.provider]:
logging.warning("ContextCSN of %s not the same of provider", srv_name)
noerror = False
if len(not_found[srv])>0:
logging.warning('Not found objects on %s :\n - %s' % (srv,'\n - '.join(not_found[srv])))
noerror=False
if len(not_sync[srv])>0:
logging.warning('Not sync objects on %s : %s' % (srv,'\n - '.join(not_sync[srv])))
noerror=False
if noerror:
logging.info('No sync problem detected')
if not options.onlycheckcontextcsn:
if not_found[srv]:
logging.warning(
"Not found objects on %s :\n - %s", srv, "\n - ".join(not_found[srv])
)
noerror = False
if not_sync[srv]:
logging.warning("Not sync objects on %s: %s", srv, "\n - ".join(not_sync[srv]))
noerror = False
if noerror:
logging.info("No sync problem detected")

1
debian/compat vendored Normal file
View file

@ -0,0 +1 @@
11

30
debian/control vendored Normal file
View file

@ -0,0 +1,30 @@
Source: check-syncrepl-extended
Section: admin
Priority: optional
Maintainer: Debian Zionetrix - check-syncrepl-extended <debian+check-syncrepl-extended@zionetrix.net>
Build-Depends: debhelper (>> 11.0.0)
Standards-Version: 3.9.6
Package: check-syncrepl-extended
Architecture: all
Depends: ${misc:Depends}, python3, python3-ldap
Description: Check LDAP syncrepl replication state between two servers
This script check LDAP syncrepl replication state between two servers. One
server is consider as provider and the other as consumer.
This script can check replication state with two method :
- by the first, entryCSN of all entries of LDAP directory will be compare
between two servers
- by the second, all values of all attributes of all entries will be compare
between two servers.
In all case, contextCSN of servers will be compare and entries not present in
consumer or in provider will be notice. You can decide to disable contextCSN
verification by using argument --no-check-contextCSN.
This script is also able to "touch" LDAP object on provider to force
synchronisation of this object. This mechanism consist to add '%%TOUCH%%'
value to an attribute of this object and remove it just after. The touched
attribute is specify by parameter --touch. Of course, couple of DN and
password provided, must have write right on this attribute.
If your prefer, you can use --replace-touch parameter to replace value of
touched attribute instead of adding the touched value. Use-ful in case of
single-value attribute.
To use this script as an Icinga (or Nagios) plugin, use -n argument

20
debian/copyright vendored Normal file
View file

@ -0,0 +1,20 @@
This package was written by Benjamin Renard <brenard@zionetrix.net>.
Copyright (C) 2022 Benjamin Renard <brenard@zionetrix.net>
check-syncrepl-extended is licensed under the GNU general public license, version 3.
check-syncrepl-extended 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; either version 2, or (at your option) any later version.
check-syncrepl-extended is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
check-syncrepl-extended; see the file COPYING. If not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
On Debian systems, a copy of the GNU General Public License is available in
/usr/share/common-licenses/GPL-3 as part of the base-files package.

1
debian/dirs vendored Normal file
View file

@ -0,0 +1 @@
usr/lib/nagios/plugins

1
debian/install vendored Normal file
View file

@ -0,0 +1 @@
check_syncrepl_extended usr/lib/nagios/plugins

4
debian/rules vendored Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/make -f
#export DH_VERBOSE=1
%:
dh $@

1
debian/source/format vendored Normal file
View file

@ -0,0 +1 @@
1.0

View file

@ -1,27 +1,24 @@
---
name: check_syncrepl_extended
description: "file:///README.md"
url: "https://gogs.zionetrix.net/bn8/check_syncrepl_extended"
url: "https://gitea.zionetrix.net/bn8/check_syncrepl_extended"
tags: OpenLDAP,syncrepl
vendor: OpenLDAP
target: Database
type: Plugin
license: gplv2
releases:
-
name: v2017.09.11
releases:
- name: v2017.09.11
description: "v2017.09.11 Release"
files:
-
name: check_syncrepl_extended
files:
- name: check_syncrepl_extended
url: "file:///check_syncrepl_extended"
description: "First release upload on Icinga Exchange"
checksum: cee2e8fdc243edcbab30d3b034e8b9bf
-
name: v2017.09.12
- name: v2017.09.12
description: "v2017.09.12 Release"
files:
-
name: check_syncrepl_extended
files:
- name: check_syncrepl_extended
url: "file:///check_syncrepl_extended"
description: "Improve ContextCSN checking (thanks Orhan)"
checksum: a954ca49855c23f2f098f4696c4c0ebd

2
setup.cfg Normal file
View file

@ -0,0 +1,2 @@
[flake8]
ignore = E501