Compare commits

..

No commits in common. "057ddf2a0b13bfccba20f36890692b22a2b80520" and "f3f8ae9f1a79085318c99576476f980e9ea68486" have entirely different histories.

9 changed files with 283 additions and 350 deletions

View file

@ -1,89 +0,0 @@
---
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

@ -1,21 +0,0 @@
---
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

View file

@ -1,64 +0,0 @@
# 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

70
.woodpecker.yml Normal file
View file

@ -0,0 +1,70 @@
clone:
git:
image: woodpeckerci/plugin-git
tags: true
pipeline:
test-pylint:
group: test
image: debian
commands:
- DEBIAN_FRONTEND=noninteractive apt-get -qq update < /dev/null > /dev/null
- DEBIAN_FRONTEND=noninteractive apt-get -qq -y install --no-install-recommends python3-ldap pylint3 < /dev/null > /dev/null
- python3 -m pylint check_syncrepl_extended
test-flake8:
group: test
image: pipelinecomponents/flake8
commands:
- flake8 check_syncrepl_extended
build:
image: brenard/debian-python-deb
when:
event: tag
commands:
- echo "$GPG_KEY"|base64 -d|gpg --import
- ./build.sh --quiet
secrets: [ maintainer_name, maintainer_email, gpg_key, debian_codename ]
publish-dryrun:
group: publish
image: alpine
when:
event: tag
commands:
- ls dist/* dist/check-syncrepl-extended-*/check_syncrepl_extended
publish-gitea:
group: publish
image: plugins/gitea-release
when:
event: tag
settings:
api_key:
from_secret: gitea_token
base_url: https://gitea.zionetrix.net
note: dist/release_notes.md
files:
- dist/check-syncrepl-extended-*/check_syncrepl_extended
- dist/*.deb
checksum:
- md5
- sha512
publish-apt:
group: publish
image: brenard/aptly-publish
when:
event: tag
settings:
api_url:
from_secret: apt_api_url
api_username:
from_secret: apt_api_username
api_password:
from_secret: apt_api_password
repo_name:
from_secret: apt_repo_name
path: dist
source_name: check-syncrepl-extended

View file

@ -1,21 +1,19 @@
# 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. This script check LDAP syncrepl replication state between two servers.
One server is consider as provider and the other as consumer. One server is consider as provider and the other as consumer.
This script can check replication state with two method : 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.
- by the first, entryCSN of all entries of LDAP directory 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 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.
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 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.
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
To use this script as an Icinga (or Nagios) plugin, use _-n_ argument
## Requirement ## Requirement
@ -23,13 +21,12 @@ A single couple of DN and password able to connect to both server and without re
## Dependencies ## Dependencies
- python 3 (for python 2.7 compatibility, see python2.7 branch) * python 3 (for python 2.7 compatibility, see python2.7 branch)
- python-ldap * python-ldap
## Installation ## Installation
### If you plan to use it with NRPE ### If you plan to use it with NRPE
``` ```
apt install -y python3-ldap git apt install -y python3-ldap git
git clone https://gitea.zionetrix.net/bn8/check_syncrepl_extended.git /usr/local/src/check_syncrepl_extended git clone https://gitea.zionetrix.net/bn8/check_syncrepl_extended.git /usr/local/src/check_syncrepl_extended
@ -42,7 +39,6 @@ service nagios-nrpe-server reload
``` ```
### Otherwise ### Otherwise
``` ```
apt install python3-ldap git apt install python3-ldap git
git clone https://gitea.zionetrix.net/bn8/check_syncrepl_extended.git /usr/local/src/check_syncrepl_extended git clone https://gitea.zionetrix.net/bn8/check_syncrepl_extended.git /usr/local/src/check_syncrepl_extended
@ -50,7 +46,6 @@ ln -s /usr/local/src/check_syncrepl_extended/check_syncrepl_extended /usr/local/
``` ```
## Usage ## Usage
``` ```
usage: check_syncrepl_extended [-h] [-v] [-p PROVIDER] [-c CONSUMER] usage: check_syncrepl_extended [-h] [-v] [-p PROVIDER] [-c CONSUMER]
[-i SERVERID] [-T] [-D DN] [-P PWD] [-b BASEDN] [-i SERVERID] [-T] [-D DN] [-P PWD] [-b BASEDN]
@ -95,7 +90,7 @@ optional arguments:
Don't check servers contextCSN (Default: False) Don't check servers contextCSN (Default: False)
-a, --attributes Check attributes values (Default: check only entryCSN) -a, --attributes Check attributes values (Default: check only entryCSN)
--exclude-attributes EXCL_ATTRS --exclude-attributes EXCL_ATTRS
Don't check this attribute (only in attribute check Don't check this attribut (only in attribute check
mode) mode)
--touch TOUCH Touch attribute giving in parameter to force resync a --touch TOUCH Touch attribute giving in parameter to force resync a
this LDAP object from provider. A value '%TOUCH%' will this LDAP object from provider. A value '%TOUCH%' will
@ -120,6 +115,6 @@ Copyright (c) 2017 Benjamin Renard
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 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. 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. 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.

View file

@ -24,7 +24,7 @@ sed -i "s/^VERSION *=.*$/VERSION = '$VERSION'/" $BDIR/check_syncrepl_extended
if [ -z "$DEBIAN_CODENAME" ] if [ -z "$DEBIAN_CODENAME" ]
then then
echo "Retrieve debian codename using lsb_release..." echo "Retreive debian codename using lsb_release..."
DEBIAN_CODENAME=$( lsb_release -c -s ) DEBIAN_CODENAME=$( lsb_release -c -s )
else else
echo "Use debian codename from environment ($DEBIAN_CODENAME)" echo "Use debian codename from environment ($DEBIAN_CODENAME)"

View file

@ -5,9 +5,9 @@
# One server is consider as provider and the other as consumer. # One server is consider as provider and the other as consumer.
# #
# This script can check replication state with two method : # This script can check replication state with two method :
# - by the first, entryCSN of all entries of LDAP directory will be # - by the fisrt, entryCSN of all entries of LDAP directory will be
# compare between two servers # compare between two servers
# - by the second, all values of all attributes of all entries will # - by the second, all values of all atributes of all entries will
# be compare between two servers. # be compare between two servers.
# #
# In all case, contextCSN of servers will be compare and entries not # In all case, contextCSN of servers will be compare and entries not
@ -35,48 +35,47 @@
# #
import argparse import argparse
import getpass
import logging import logging
import sys import sys
import getpass
import ldap import ldap
from ldap import LDAPError # pylint: disable=no-name-in-module from ldap import LDAPError # pylint: disable=no-name-in-module
from ldap import modlist
from ldap.controls import SimplePagedResultsControl from ldap.controls import SimplePagedResultsControl
from ldap import modlist
VERSION = "0.0" VERSION = '0.0'
TOUCH_VALUE = b"%%TOUCH%%" TOUCH_VALUE = b'%%TOUCH%%'
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=("Script to check LDAP syncrepl replication state between two servers."), description=(
"Script to check LDAP syncrepl replication state between "
"two servers."),
epilog=( epilog=(
"Author: Benjamin Renard <brenard@easter-eggs.com>, " 'Author: Benjamin Renard <brenard@easter-eggs.com>, '
f"Version: {VERSION}, " f'Version: {VERSION}, '
"Source: https://gitea.zionetrix.net/bn8/check_syncrepl_extended" 'Source: https://gitea.zionetrix.net/bn8/check_syncrepl_extended')
),
) )
parser.add_argument( parser.add_argument(
"-p", "-p", "--provider",
"--provider",
dest="provider", dest="provider",
action="store", action="store",
type=str, type=str,
help="LDAP provider URI (example: ldaps://ldapmaster.foo:636)", help="LDAP provider URI (example: ldaps://ldapmaster.foo:636)"
) )
parser.add_argument( parser.add_argument(
"-c", "-c", "--consumer",
"--consumer",
dest="consumer", dest="consumer",
action="store", action="store",
type=str, type=str,
help="LDAP consumer URI (example: ldaps://ldapslave.foo:636)", help="LDAP consumer URI (example: ldaps://ldapslave.foo:636)"
) )
parser.add_argument( parser.add_argument(
"-i", "-i", "--serverID",
"--serverID",
dest="serverid", dest="serverid",
action="store", action="store",
type=int, type=int,
@ -85,67 +84,74 @@ parser.add_argument(
"setups where each master has a unique ID and a contextCSN for " "setups where each master has a unique ID and a contextCSN for "
"each replicated master exists. A valid serverID is a integer " "each replicated master exists. A valid serverID is a integer "
"value from 0 to 4095 (limited to 3 hex digits, example: '12' " "value from 0 to 4095 (limited to 3 hex digits, example: '12' "
"compares the contextCSN matching '#00C#')" "compares the contextCSN matching '#00C#')"),
), default=False
default=False,
) )
parser.add_argument( parser.add_argument(
"-T", "-T", "--starttls",
"--starttls",
dest="starttls", dest="starttls",
action="store_true", action="store_true",
help="Start TLS on LDAP provider/consumers connections", help="Start TLS on LDAP provider/consumers connections",
default=False, default=False
) )
parser.add_argument( parser.add_argument(
"-D", "-D", "--dn",
"--dn",
dest="dn", dest="dn",
action="store", action="store",
type=str, type=str,
help="LDAP bind DN (example: uid=nagios,ou=sysaccounts,o=example", help="LDAP bind DN (example: uid=nagios,ou=sysaccounts,o=example"
) )
parser.add_argument( parser.add_argument(
"-P", "--pwd", dest="pwd", action="store", type=str, help="LDAP bind password", default=None "-P", "--pwd",
dest="pwd",
action="store",
type=str,
help="LDAP bind password",
default=None
) )
parser.add_argument( parser.add_argument(
"-b", "-b", "--basedn",
"--basedn",
dest="basedn", dest="basedn",
action="store", action="store",
type=str, type=str,
help="LDAP base DN (example: o=example)", help="LDAP base DN (example: o=example)"
) )
parser.add_argument( parser.add_argument(
"-f", "-f", "--filter",
"--filter",
dest="filterstr", dest="filterstr",
action="store", action="store",
type=str, type=str,
help="LDAP filter (default: (objectClass=*))", help="LDAP filter (default: (objectClass=*))",
default="(objectClass=*)", default='(objectClass=*)'
) )
parser.add_argument( parser.add_argument(
"-d", "--debug", dest="debug", action="store_true", help="Debug mode", default=False "-d", "--debug",
dest="debug",
action="store_true",
help="Debug mode",
default=False
) )
parser.add_argument( parser.add_argument(
"-n", "-n", "--nagios",
"--nagios",
dest="nagios", dest="nagios",
action="store_true", action="store_true",
help="Nagios check plugin mode", help="Nagios check plugin mode",
default=False, default=False
) )
parser.add_argument( parser.add_argument(
"-q", "--quiet", dest="quiet", action="store_true", help="Quiet mode", default=False "-q", "--quiet",
dest="quiet",
action="store_true",
help="Quiet mode",
default=False
) )
parser.add_argument( parser.add_argument(
@ -153,7 +159,7 @@ parser.add_argument(
dest="nocheckcert", dest="nocheckcert",
action="store_true", action="store_true",
help="Don't check the server certificate (Default: False)", help="Don't check the server certificate (Default: False)",
default=False, default=False
) )
parser.add_argument( parser.add_argument(
@ -161,24 +167,25 @@ parser.add_argument(
dest="nocheckcontextcsn", dest="nocheckcontextcsn",
action="store_true", action="store_true",
help="Don't check servers contextCSN (Default: False)", help="Don't check servers contextCSN (Default: False)",
default=False, default=False
) )
parser.add_argument( parser.add_argument(
"--only-check-contextCSN", "--only-check-contextCSN",
dest="onlycheckcontextcsn", dest="onlycheckcontextcsn",
action="store_true", action="store_true",
help=("Only check servers root contextCSN (objects check disabled, default : False)"), help=(
default=False, "Only check servers root contextCSN (objects check disabled, "
"default : False)"),
default=False
) )
parser.add_argument( parser.add_argument(
"-a", "-a", "--attributes",
"--attributes",
dest="attrs", dest="attrs",
action="store_true", action="store_true",
help="Check attributes values (Default: check only entryCSN)", help="Check attributes values (Default: check only entryCSN)",
default=False, default=False
) )
parser.add_argument( parser.add_argument(
@ -186,8 +193,8 @@ parser.add_argument(
dest="excl_attrs", dest="excl_attrs",
action="store", action="store",
type=str, type=str,
help="Don't check this attribute (only in attribute check mode)", help="Don't check this attribut (only in attribute check mode)",
default=None, default=None
) )
parser.add_argument( parser.add_argument(
@ -196,13 +203,13 @@ parser.add_argument(
action="store", action="store",
type=str, type=str,
help=( help=(
"Touch attribute giving in parameter to force resync a this LDAP " 'Touch attribute giving in parameter to force resync a this LDAP '
f'object from provider. A value "{TOUCH_VALUE.decode()}" will be ' f'object from provider. A value "{TOUCH_VALUE.decode()}" will be '
"add to this attribute and remove after. The user use to connect " 'add to this attribute and remove after. The user use to connect '
"to the LDAP directory must have write permission on this " 'to the LDAP directory must have write permission on this '
"attribute on each object." 'attribute on each object.'
), ),
default=None, default=None
) )
parser.add_argument( parser.add_argument(
@ -210,7 +217,7 @@ parser.add_argument(
dest="replacetouch", dest="replacetouch",
action="store_true", action="store_true",
help="In touch mode, replace value instead of adding.", help="In touch mode, replace value instead of adding.",
default=False, default=False
) )
parser.add_argument( parser.add_argument(
@ -218,7 +225,7 @@ parser.add_argument(
dest="removetouchvalue", dest="removetouchvalue",
action="store_true", action="store_true",
help="In touch mode, remove touch value if present.", help="In touch mode, remove touch value if present.",
default=False, default=False
) )
parser.add_argument( parser.add_argument(
@ -226,8 +233,10 @@ parser.add_argument(
dest="page_size", dest="page_size",
action="store", action="store",
type=int, type=int,
help=("Page size: if defined, paging control using LDAP v3 extended control will be enabled."), help=(
default=None, "Page size: if defined, paging control using LDAP v3 extended "
"control will be enabled."),
default=None
) )
options = parser.parse_args() options = parser.parse_args()
@ -235,8 +244,7 @@ options = parser.parse_args()
if options.nocheckcontextcsn and options.onlycheckcontextcsn: if options.nocheckcontextcsn and options.onlycheckcontextcsn:
parser.error( parser.error(
"You can't use both --no-check-contextCSN and " "You can't use both --no-check-contextCSN and "
"--only-check-contextCSN parameters and the same time" "--only-check-contextCSN parameters and the same time")
)
if options.nagios: if options.nagios:
sys.exit(3) sys.exit(3)
sys.exit(1) sys.exit(1)
@ -256,14 +264,14 @@ if not options.basedn:
if not 0 <= options.serverid <= 4095: if not 0 <= options.serverid <= 4095:
parser.error( parser.error(
"ServerID should be a integer value from 0 to 4095 (limited to 3 hexadecimal digits)." "ServerID should be a integer value from 0 to 4095 "
) "(limited to 3 hexadecimal digits).")
if options.nagios: if options.nagios:
sys.exit(3) sys.exit(3)
sys.exit(1) sys.exit(1)
if options.touch and not options.attrs: if options.touch and not options.attrs:
logging.info("Force option attrs on touch mode") logging.info('Force option attrs on touch mode')
options.attrs = True options.attrs = True
if options.dn and options.pwd is None: if options.dn and options.pwd is None:
@ -271,7 +279,7 @@ if options.dn and options.pwd is None:
excl_attrs = [] excl_attrs = []
if options.excl_attrs: if options.excl_attrs:
for ex in options.excl_attrs.split(","): for ex in options.excl_attrs.split(','):
excl_attrs.append(ex.strip()) excl_attrs.append(ex.strip())
FORMAT = "%(asctime)s - %(levelname)s: %(message)s" FORMAT = "%(asctime)s - %(levelname)s: %(message)s"
@ -288,9 +296,10 @@ else:
class LdapServer: class LdapServer:
uri = None
dn = None uri = ""
pwd = None dn = ""
pwd = ""
start_tls = False start_tls = False
con = 0 con = 0
@ -321,20 +330,21 @@ class LdapServer:
def getContextCSN(self, basedn=False, serverid=False): def getContextCSN(self, basedn=False, serverid=False):
if not basedn: if not basedn:
basedn = self.dn basedn = self.dn
data = self.search(basedn, "(objectclass=*)", attrs=["contextCSN"], scope="base") data = self.search(
basedn, '(objectclass=*)', attrs=['contextCSN'], scope='base')
if data: if data:
contextCSNs = data[0][0][1]["contextCSN"] contextCSNs = data[0][0][1]['contextCSN']
logging.debug("Found contextCSNs %s", contextCSNs) logging.debug('Found contextCSNs %s', contextCSNs)
if serverid is False: if serverid is False:
return contextCSNs[0] return contextCSNs[0]
csnid = str(format(serverid, "X")).zfill(3) csnid = str(format(serverid, 'X')).zfill(3)
sub = str.encode(f"#{csnid}#", encoding="ascii", errors="replace") sub = str.encode(f'#{csnid}#', encoding="ascii", errors="replace")
CSN = [s for s in contextCSNs if sub in s] CSN = [s for s in contextCSNs if sub in s]
if not CSN: if not CSN:
logging.error( logging.error(
"No contextCSN matching with ServerID %s (=%s) could be found.", "No contextCSN matching with ServerID %s (=%s) could be "
serverid, "found.",
sub, serverid, sub
) )
return False return False
return CSN[0] return CSN[0]
@ -342,19 +352,21 @@ class LdapServer:
@staticmethod @staticmethod
def get_scope(scope): def get_scope(scope):
if scope == "base": if scope == 'base':
return ldap.SCOPE_BASE # pylint: disable=no-member return ldap.SCOPE_BASE # pylint: disable=no-member
if scope == "one": if scope == 'one':
return ldap.SCOPE_ONELEVEL # pylint: disable=no-member return ldap.SCOPE_ONELEVEL # pylint: disable=no-member
if scope == "sub": if scope == 'sub':
return ldap.SCOPE_SUBTREE # pylint: disable=no-member return ldap.SCOPE_SUBTREE # pylint: disable=no-member
raise Exception(f'Unknown LDAP scope "{scope}"') # pylint: disable=broad-exception-raised raise Exception(f'Unknown LDAP scope "{scope}"')
def search(self, basedn, filterstr, attrs=None, scope=None): def search(self, basedn, filterstr, attrs=None, scope=None):
if self.page_size: if self.page_size:
return self.paged_search(basedn, filterstr, attrs=attrs, scope=scope) return self.paged_search(
basedn, filterstr, attrs=attrs, scope=scope)
res_id = self.con.search( res_id = self.con.search(
basedn, self.get_scope(scope if scope else "sub"), filterstr, attrs if attrs else [] basedn, self.get_scope(scope if scope else 'sub'),
filterstr, attrs if attrs else []
) )
ret = [] ret = []
while 1: while 1:
@ -368,16 +380,13 @@ class LdapServer:
def paged_search(self, basedn, filterstr, attrs=None, scope=None): def paged_search(self, basedn, filterstr, attrs=None, scope=None):
ret = [] ret = []
page = 0 page = 0
pg_ctrl = SimplePagedResultsControl(True, self.page_size, "") pg_ctrl = SimplePagedResultsControl(True, self.page_size, '')
while page == 0 or pg_ctrl.cookie: while page == 0 or pg_ctrl.cookie:
page += 1 page += 1
logging.debug("Page search: loading page %d", page) logging.debug('Page search: loading page %d', page)
res_id = self.con.search_ext( res_id = self.con.search_ext(
basedn, basedn, self.get_scope(scope if scope else 'sub'),
self.get_scope(scope if scope else "sub"), filterstr, attrs if attrs else [], serverctrls=[pg_ctrl]
filterstr,
attrs if attrs else [],
serverctrls=[pg_ctrl],
) )
# pylint: disable=unused-variable # pylint: disable=unused-variable
res_type, res_data, res_id, serverctrls = self.con.result3(res_id) res_type, res_data, res_id, serverctrls = self.con.result3(res_id)
@ -394,11 +403,11 @@ class LdapServer:
if not ldif: if not ldif:
return True return True
try: try:
logging.debug("Update object %s: %s", dn, ldif) logging.debug('Update object %s: %s', dn, ldif)
self.con.modify_s(dn, ldif) self.con.modify_s(dn, ldif)
return True return True
except LDAPError: except LDAPError:
logging.error("Error updating object %s", dn, exc_info=True) logging.error('Error updating object %s', dn, exc_info=True)
return False return False
@staticmethod @staticmethod
@ -423,9 +432,14 @@ class LdapServer:
else: else:
new[attr].append(TOUCH_VALUE) new[attr].append(TOUCH_VALUE)
try: try:
logging.info('Touch object "%s" on attribute "%s": %s => %s', dn, attr, old, new) logging.info(
'Touch object "%s" on attribute "%s": %s => %s',
dn, attr, old, new
)
if self.update_object(dn, old, new): if self.update_object(dn, old, new):
logging.info('Restore original value of attribute "%s" of object "%s"', attr, dn) logging.info(
'Restore original value of attribute "%s" of object "%s"',
attr, dn)
if options.removetouchvalue and TOUCH_VALUE in old[attr]: if options.removetouchvalue and TOUCH_VALUE in old[attr]:
old[attr].remove(TOUCH_VALUE) old[attr].remove(TOUCH_VALUE)
self.update_object(dn=dn, old=new, new=old) self.update_object(dn=dn, old=new, new=old)
@ -437,7 +451,8 @@ class LdapServer:
if options.nocheckcert: if options.nocheckcert:
# pylint: disable=no-member # pylint: disable=no-member
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) ldap.set_option(
ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
servers = [options.provider, options.consumer] servers = [options.provider, options.consumer]
@ -446,36 +461,44 @@ LdapObjects = {}
LdapServersCSN = {} LdapServersCSN = {}
for srv in servers: for srv in servers:
logging.info("Connect to %s", srv) logging.info('Connect to %s', srv)
LdapServers[srv] = LdapServer( LdapServers[srv] = LdapServer(srv, options.dn, options.pwd,
srv, options.dn, options.pwd, options.starttls, page_size=options.page_size options.starttls,
) page_size=options.page_size)
if not LdapServers[srv].connect(): if not LdapServers[srv].connect():
if options.nagios: if options.nagios:
print(f"UNKWNON - Failed to connect to {srv}") print(f'UNKWNON - Failed to connect to {srv}')
sys.exit(3) sys.exit(3)
else: else:
sys.exit(1) sys.exit(1)
if not options.nocheckcontextcsn: if not options.nocheckcontextcsn:
LdapServersCSN[srv] = LdapServers[srv].getContextCSN(options.basedn, options.serverid) LdapServersCSN[srv] = LdapServers[srv].getContextCSN(
logging.info("ContextCSN of %s: %s", srv, LdapServersCSN[srv]) options.basedn, options.serverid)
logging.info('ContextCSN of %s: %s', srv, LdapServersCSN[srv])
if not options.onlycheckcontextcsn: if not options.onlycheckcontextcsn:
logging.info("List objects from %s", srv) logging.info('List objects from %s', srv)
LdapObjects[srv] = {} LdapObjects[srv] = {}
if options.attrs: if options.attrs:
for obj in LdapServers[srv].search(options.basedn, options.filterstr, []): for obj in LdapServers[srv].search(
logging.debug("Found on %s: %s", srv, obj[0][0]) options.basedn, options.filterstr, []
):
logging.debug('Found on %s: %s', srv, obj[0][0])
LdapObjects[srv][obj[0][0]] = obj[0][1] LdapObjects[srv][obj[0][0]] = obj[0][1]
else: else:
for obj in LdapServers[srv].search(options.basedn, options.filterstr, ["entryCSN"]): for obj in LdapServers[srv].search(
logging.debug("Found on %s: %s / %s", srv, obj[0][0], obj[0][1]["entryCSN"][0]) options.basedn, options.filterstr, ['entryCSN']
LdapObjects[srv][obj[0][0]] = obj[0][1]["entryCSN"][0] ):
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]))
if not options.onlycheckcontextcsn: if not options.onlycheckcontextcsn:
@ -487,11 +510,14 @@ if not options.onlycheckcontextcsn:
not_sync[srv] = [] not_sync[srv] = []
if options.attrs: if options.attrs:
logging.info("Check if objects a are synchronized (by comparing attributes's values)") logging.info(
"Check if objects a are synchronized (by comparing attributes's "
"values)")
else: else:
logging.info("Check if objects are synchronized (by comparing entryCSN)") logging.info(
'Check if objets are synchronized (by comparing entryCSN)')
for obj in LdapObjects[options.provider]: for obj in LdapObjects[options.provider]:
logging.debug("Check obj %s", obj) logging.debug('Check obj %s', obj)
for srv_name, srv in LdapObjects.items(): for srv_name, srv in LdapObjects.items():
if srv_name == options.provider: if srv_name == options.provider:
continue continue
@ -507,9 +533,7 @@ if not options.onlycheckcontextcsn:
attrs_list.append(attr) attrs_list.append(attr)
logging.debug( logging.debug(
"Obj %s not synchronized: %s not present on %s", "Obj %s not synchronized: %s not present on %s",
obj, obj, ','.join(attrs_list), srv_name
",".join(attrs_list),
srv_name,
) )
touch = True touch = True
else: else:
@ -519,8 +543,7 @@ if not options.onlycheckcontextcsn:
attrs_list.append(attr) attrs_list.append(attr)
logging.debug( logging.debug(
"Obj %s not synchronized: %s not same value(s)", "Obj %s not synchronized: %s not same value(s)",
obj, obj, ','.join(attrs_list)
",".join(attrs_list),
) )
touch = True touch = True
if attrs_list: if attrs_list:
@ -528,29 +551,29 @@ if not options.onlycheckcontextcsn:
else: else:
logging.debug( logging.debug(
"Obj %s not synchronized: %s <-> %s", "Obj %s not synchronized: %s <-> %s",
obj, obj, LdapObjects[options.provider][obj], srv[obj]
LdapObjects[options.provider][obj],
srv[obj],
) )
not_sync[srv_name].append(obj) not_sync[srv_name].append(obj)
if touch and options.touch: if touch and options.touch:
orig_value = [] orig_value = []
if options.touch in LdapObjects[options.provider][obj]: if options.touch in LdapObjects[options.provider][obj]:
orig_value = LdapObjects[options.provider][obj][options.touch] orig_value = LdapObjects[options.provider][obj][options.touch]
LdapServers[options.provider].touch_object(obj, options.touch, orig_value) LdapServers[options.provider].touch_object(
obj, options.touch, orig_value)
else: else:
logging.debug("Obj %s: not found on %s", obj, srv_name) logging.debug('Obj %s: not found on %s', obj, srv_name)
not_found[srv_name].append(obj) not_found[srv_name].append(obj)
if options.touch: if options.touch:
orig_value = [] orig_value = []
if options.touch in LdapObjects[options.provider][obj]: if options.touch in LdapObjects[options.provider][obj]:
orig_value = LdapObjects[options.provider][obj][options.touch] orig_value = LdapObjects[options.provider][obj][options.touch]
LdapServers[options.provider].touch_object(obj, options.touch, orig_value) LdapServers[options.provider].touch_object(
obj, options.touch, orig_value)
for obj in LdapObjects[options.consumer]: for obj in LdapObjects[options.consumer]:
logging.debug("Check obj %s of consumer", obj) logging.debug('Check obj %s of consumer', obj)
if obj not in LdapObjects[options.provider]: if obj not in LdapObjects[options.provider]:
logging.debug("Obj %s: not found on provider", obj) logging.debug('Obj %s: not found on provider', obj)
not_found[options.provider].append(obj) not_found[options.provider].append(obj)
if options.nagios: if options.nagios:
@ -559,75 +582,91 @@ if options.nagios:
if not options.nocheckcontextcsn: if not options.nocheckcontextcsn:
if not LdapServersCSN[options.provider]: if not LdapServersCSN[options.provider]:
errors.append("ContextCSN of LDAP server provider could not be found") errors.append('ContextCSN of LDAP server provider could not be found')
else: else:
long_output.append( long_output.append(
f"ContextCSN on LDAP server provider = {LdapServersCSN[options.provider]}" f'ContextCSN on LDAP server provider = {LdapServersCSN[options.provider]}')
)
for srv_name, srv_csn in LdapServersCSN.items(): for srv_name, srv_csn in LdapServersCSN.items():
if srv_name == options.provider: if srv_name == options.provider:
continue continue
if not srv_csn: if not srv_csn:
errors.append(f"ContextCSN of {srv_name} not found") errors.append(f'ContextCSN of {srv_name} not found')
elif srv_csn != LdapServersCSN[options.provider]: elif srv_csn != LdapServersCSN[options.provider]:
errors.append(f"ContextCSN of {srv_name} not the same of provider") errors.append(
long_output.append(f"ContextCSN on LDAP server {srv_name} = {srv_csn}") f'ContextCSN of {srv_name} not the same of provider')
long_output.append(
f'ContextCSN on LDAP server {srv_name} = {srv_csn}')
if not options.onlycheckcontextcsn: if not options.onlycheckcontextcsn:
if not_found[options.consumer]: if not_found[options.consumer]:
errors.append(f"{len(not_found[options.consumer])} not found object(s) on consumer") errors.append(
long_output.append(f"Object(s) not found on server {options.consumer} (consumer):") 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]: for obj in not_found[options.consumer]:
long_output.append(f" - {obj}") long_output.append(f' - {obj}')
if not_found[options.provider]: if not_found[options.provider]:
errors.append(f"{len(not_found[options.provider])} not found object(s) on provider") errors.append(
long_output.append(f"Object(s) not found on server {options.provider} (provider):") 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]: for obj in not_found[options.provider]:
long_output.append(f" - {obj}") long_output.append(f' - {obj}')
if not_sync[options.consumer]: if not_sync[options.consumer]:
errors.append( errors.append(
f"{len(not_sync[options.consumer])} not synchronized object(s) on consumer" f'{len(not_sync[options.consumer])} not synchronized object(s) '
) 'on consumer')
long_output.append( long_output.append(
f"Object(s) not synchronized on server {options.consumer} (consumer):" f'Object(s) not synchronized on server {options.consumer} '
) '(consumer):')
for obj in not_sync[options.consumer]: for obj in not_sync[options.consumer]:
long_output.append(f" - {obj}") long_output.append(f' - {obj}')
if errors: if errors:
print(f'CRITICAL: {", ".join(errors)}') print(f'CRITICAL: {", ".join(errors)}')
print("\n\n") print('\n\n')
print("\n".join(long_output)) print("\n".join(long_output))
sys.exit(2) sys.exit(2)
else: else:
print("OK: consumer and provider are synchronized") print('OK: consumer and provider are synchronized')
sys.exit(0) sys.exit(0)
else: else:
noerror = True noerror = True
for srv in servers: for srv in servers:
if not options.nocheckcontextcsn: if not options.nocheckcontextcsn:
if not LdapServersCSN[options.provider]: if not LdapServersCSN[options.provider]:
logging.warning("ContextCSN of LDAP server provider could not be found") logging.warning(
'ContextCSN of LDAP server provider could not be found')
noerror = False noerror = False
else: else:
for srv_name, srv_csn in LdapServersCSN.items(): for srv_name, srv_csn in LdapServersCSN.items():
if srv_name == options.provider: if srv_name == options.provider:
continue continue
if not srv_csn: if not srv_csn:
logging.warning("ContextCSN of %s not found", srv_name) logging.warning('ContextCSN of %s not found', srv_name)
noerror = False noerror = False
elif srv_csn != LdapServersCSN[options.provider]: elif srv_csn != LdapServersCSN[options.provider]:
logging.warning("ContextCSN of %s not the same of provider", srv_name) logging.warning(
'ContextCSN of %s not the same of provider',
srv_name)
noerror = False noerror = False
if not options.onlycheckcontextcsn: if not options.onlycheckcontextcsn:
if not_found[srv]: if not_found[srv]:
logging.warning( logging.warning(
"Not found objects on %s :\n - %s", srv, "\n - ".join(not_found[srv]) 'Not found objects on %s :\n - %s',
srv, '\n - '.join(not_found[srv])
) )
noerror = False noerror = False
if not_sync[srv]: if not_sync[srv]:
logging.warning("Not sync objects on %s: %s", srv, "\n - ".join(not_sync[srv])) logging.warning(
'Not sync objects on %s: %s',
srv, '\n - '.join(not_sync[srv])
)
noerror = False noerror = False
if noerror: if noerror:
logging.info("No sync problem detected") logging.info('No sync problem detected')

4
debian/control vendored
View file

@ -12,9 +12,9 @@ Description: Check LDAP syncrepl replication state between two servers
This script check LDAP syncrepl replication state between two servers. One This script check LDAP syncrepl replication state between two servers. One
server is consider as provider and the other as consumer. server is consider as provider and the other as consumer.
This script can check replication state with two method : This script can check replication state with two method :
- by the first, entryCSN of all entries of LDAP directory will be compare - by the fisrt, entryCSN of all entries of LDAP directory will be compare
between two servers between two servers
- by the second, all values of all attributes of all entries will be compare - by the second, all values of all atributes of all entries will be compare
between two servers. between two servers.
In all case, contextCSN of servers will be compare and entries not present in 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 consumer or in provider will be notice. You can decide to disable contextCSN

View file

@ -1,4 +1,3 @@
---
name: check_syncrepl_extended name: check_syncrepl_extended
description: "file:///README.md" description: "file:///README.md"
url: "https://gitea.zionetrix.net/bn8/check_syncrepl_extended" url: "https://gitea.zionetrix.net/bn8/check_syncrepl_extended"
@ -8,17 +7,21 @@ target: Database
type: Plugin type: Plugin
license: gplv2 license: gplv2
releases: releases:
- name: v2017.09.11 -
name: v2017.09.11
description: "v2017.09.11 Release" description: "v2017.09.11 Release"
files: files:
- name: check_syncrepl_extended -
name: check_syncrepl_extended
url: "file:///check_syncrepl_extended" url: "file:///check_syncrepl_extended"
description: "First release upload on Icinga Exchange" description: "First release upload on Icinga Exchange"
checksum: cee2e8fdc243edcbab30d3b034e8b9bf checksum: cee2e8fdc243edcbab30d3b034e8b9bf
- name: v2017.09.12 -
name: v2017.09.12
description: "v2017.09.12 Release" description: "v2017.09.12 Release"
files: files:
- name: check_syncrepl_extended -
name: check_syncrepl_extended
url: "file:///check_syncrepl_extended" url: "file:///check_syncrepl_extended"
description: "Improve ContextCSN checking (thanks Orhan)" description: "Improve ContextCSN checking (thanks Orhan)"
checksum: a954ca49855c23f2f098f4696c4c0ebd checksum: a954ca49855c23f2f098f4696c4c0ebd