Compare commits
2 commits
f3f8ae9f1a
...
057ddf2a0b
Author | SHA1 | Date | |
---|---|---|---|
057ddf2a0b | |||
022c8fe7f6 |
9 changed files with 350 additions and 283 deletions
89
.forgejo/workflows/release.yaml
Normal file
89
.forgejo/workflows/release.yaml
Normal 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 }}
|
21
.forgejo/workflows/tests.yaml
Normal file
21
.forgejo/workflows/tests.yaml
Normal 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
|
64
.pre-commit-config.yaml
Normal file
64
.pre-commit-config.yaml
Normal 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
|
|
@ -1,70 +0,0 @@
|
|||
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
|
29
README.md
29
README.md
|
@ -1,19 +1,21 @@
|
|||
# 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.
|
||||
|
||||
To use this script as an Icinga (or Nagios) plugin, use _-n_ argument
|
||||
|
||||
## Requirement
|
||||
|
||||
|
@ -21,12 +23,13 @@ A single couple of DN and password able to connect to both server and without re
|
|||
|
||||
## Dependencies
|
||||
|
||||
* python 3 (for python 2.7 compatibility, see python2.7 branch)
|
||||
* python-ldap
|
||||
- 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
|
||||
|
@ -39,6 +42,7 @@ 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
|
||||
|
@ -46,6 +50,7 @@ ln -s /usr/local/src/check_syncrepl_extended/check_syncrepl_extended /usr/local/
|
|||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
usage: check_syncrepl_extended [-h] [-v] [-p PROVIDER] [-c CONSUMER]
|
||||
[-i SERVERID] [-T] [-D DN] [-P PWD] [-b BASEDN]
|
||||
|
@ -90,7 +95,7 @@ optional arguments:
|
|||
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 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
|
||||
|
@ -115,6 +120,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 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.
|
||||
|
|
2
build.sh
2
build.sh
|
@ -24,7 +24,7 @@ sed -i "s/^VERSION *=.*$/VERSION = '$VERSION'/" $BDIR/check_syncrepl_extended
|
|||
|
||||
if [ -z "$DEBIAN_CODENAME" ]
|
||||
then
|
||||
echo "Retreive debian codename using lsb_release..."
|
||||
echo "Retrieve debian codename using lsb_release..."
|
||||
DEBIAN_CODENAME=$( lsb_release -c -s )
|
||||
else
|
||||
echo "Use debian codename from environment ($DEBIAN_CODENAME)"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
# 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
|
||||
|
@ -35,47 +35,48 @@
|
|||
#
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import getpass
|
||||
|
||||
import ldap
|
||||
from ldap import LDAPError # pylint: disable=no-name-in-module
|
||||
from ldap.controls import SimplePagedResultsControl
|
||||
from ldap import modlist
|
||||
from ldap.controls import SimplePagedResultsControl
|
||||
|
||||
VERSION = '0.0'
|
||||
TOUCH_VALUE = b'%%TOUCH%%'
|
||||
VERSION = "0.0"
|
||||
TOUCH_VALUE = b"%%TOUCH%%"
|
||||
|
||||
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=(
|
||||
'Author: Benjamin Renard <brenard@easter-eggs.com>, '
|
||||
f'Version: {VERSION}, '
|
||||
'Source: https://gitea.zionetrix.net/bn8/check_syncrepl_extended')
|
||||
"Author: Benjamin Renard <brenard@easter-eggs.com>, "
|
||||
f"Version: {VERSION}, "
|
||||
"Source: https://gitea.zionetrix.net/bn8/check_syncrepl_extended"
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-p", "--provider",
|
||||
"-p",
|
||||
"--provider",
|
||||
dest="provider",
|
||||
action="store",
|
||||
type=str,
|
||||
help="LDAP provider URI (example: ldaps://ldapmaster.foo:636)"
|
||||
help="LDAP provider URI (example: ldaps://ldapmaster.foo:636)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-c", "--consumer",
|
||||
"-c",
|
||||
"--consumer",
|
||||
dest="consumer",
|
||||
action="store",
|
||||
type=str,
|
||||
help="LDAP consumer URI (example: ldaps://ldapslave.foo:636)"
|
||||
help="LDAP consumer URI (example: ldaps://ldapslave.foo:636)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-i", "--serverID",
|
||||
"-i",
|
||||
"--serverID",
|
||||
dest="serverid",
|
||||
action="store",
|
||||
type=int,
|
||||
|
@ -84,74 +85,67 @@ parser.add_argument(
|
|||
"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
|
||||
"compares the contextCSN matching '#00C#')"
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-T", "--starttls",
|
||||
"-T",
|
||||
"--starttls",
|
||||
dest="starttls",
|
||||
action="store_true",
|
||||
help="Start TLS on LDAP provider/consumers connections",
|
||||
default=False
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-D", "--dn",
|
||||
"-D",
|
||||
"--dn",
|
||||
dest="dn",
|
||||
action="store",
|
||||
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(
|
||||
"-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(
|
||||
"-b", "--basedn",
|
||||
"-b",
|
||||
"--basedn",
|
||||
dest="basedn",
|
||||
action="store",
|
||||
type=str,
|
||||
help="LDAP base DN (example: o=example)"
|
||||
help="LDAP base DN (example: o=example)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-f", "--filter",
|
||||
"-f",
|
||||
"--filter",
|
||||
dest="filterstr",
|
||||
action="store",
|
||||
type=str,
|
||||
help="LDAP filter (default: (objectClass=*))",
|
||||
default='(objectClass=*)'
|
||||
default="(objectClass=*)",
|
||||
)
|
||||
|
||||
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(
|
||||
"-n", "--nagios",
|
||||
"-n",
|
||||
"--nagios",
|
||||
dest="nagios",
|
||||
action="store_true",
|
||||
help="Nagios check plugin mode",
|
||||
default=False
|
||||
default=False,
|
||||
)
|
||||
|
||||
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(
|
||||
|
@ -159,7 +153,7 @@ parser.add_argument(
|
|||
dest="nocheckcert",
|
||||
action="store_true",
|
||||
help="Don't check the server certificate (Default: False)",
|
||||
default=False
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
|
@ -167,25 +161,24 @@ parser.add_argument(
|
|||
dest="nocheckcontextcsn",
|
||||
action="store_true",
|
||||
help="Don't check servers contextCSN (Default: False)",
|
||||
default=False
|
||||
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
|
||||
help=("Only check servers root contextCSN (objects check disabled, default : False)"),
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-a", "--attributes",
|
||||
"-a",
|
||||
"--attributes",
|
||||
dest="attrs",
|
||||
action="store_true",
|
||||
help="Check attributes values (Default: check only entryCSN)",
|
||||
default=False
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
|
@ -193,8 +186,8 @@ parser.add_argument(
|
|||
dest="excl_attrs",
|
||||
action="store",
|
||||
type=str,
|
||||
help="Don't check this attribut (only in attribute check mode)",
|
||||
default=None
|
||||
help="Don't check this attribute (only in attribute check mode)",
|
||||
default=None,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
|
@ -203,13 +196,13 @@ parser.add_argument(
|
|||
action="store",
|
||||
type=str,
|
||||
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 '
|
||||
'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.'
|
||||
"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
|
||||
default=None,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
|
@ -217,7 +210,7 @@ parser.add_argument(
|
|||
dest="replacetouch",
|
||||
action="store_true",
|
||||
help="In touch mode, replace value instead of adding.",
|
||||
default=False
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
|
@ -225,7 +218,7 @@ parser.add_argument(
|
|||
dest="removetouchvalue",
|
||||
action="store_true",
|
||||
help="In touch mode, remove touch value if present.",
|
||||
default=False
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
|
@ -233,10 +226,8 @@ parser.add_argument(
|
|||
dest="page_size",
|
||||
action="store",
|
||||
type=int,
|
||||
help=(
|
||||
"Page size: if defined, paging control using LDAP v3 extended "
|
||||
"control will be enabled."),
|
||||
default=None
|
||||
help=("Page size: if defined, paging control using LDAP v3 extended control will be enabled."),
|
||||
default=None,
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
@ -244,7 +235,8 @@ 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")
|
||||
"--only-check-contextCSN parameters and the same time"
|
||||
)
|
||||
if options.nagios:
|
||||
sys.exit(3)
|
||||
sys.exit(1)
|
||||
|
@ -264,14 +256,14 @@ if not options.basedn:
|
|||
|
||||
if not 0 <= options.serverid <= 4095:
|
||||
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:
|
||||
sys.exit(3)
|
||||
sys.exit(1)
|
||||
|
||||
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
|
||||
|
||||
if options.dn and options.pwd is None:
|
||||
|
@ -279,7 +271,7 @@ if options.dn and options.pwd is None:
|
|||
|
||||
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())
|
||||
|
||||
FORMAT = "%(asctime)s - %(levelname)s: %(message)s"
|
||||
|
@ -296,10 +288,9 @@ else:
|
|||
|
||||
|
||||
class LdapServer:
|
||||
|
||||
uri = ""
|
||||
dn = ""
|
||||
pwd = ""
|
||||
uri = None
|
||||
dn = None
|
||||
pwd = None
|
||||
start_tls = False
|
||||
|
||||
con = 0
|
||||
|
@ -330,21 +321,20 @@ class LdapServer:
|
|||
def getContextCSN(self, basedn=False, serverid=False):
|
||||
if not basedn:
|
||||
basedn = self.dn
|
||||
data = self.search(
|
||||
basedn, '(objectclass=*)', attrs=['contextCSN'], scope='base')
|
||||
data = self.search(basedn, "(objectclass=*)", attrs=["contextCSN"], scope="base")
|
||||
if data:
|
||||
contextCSNs = data[0][0][1]['contextCSN']
|
||||
logging.debug('Found contextCSNs %s', contextCSNs)
|
||||
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")
|
||||
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
|
||||
"No contextCSN matching with ServerID %s (=%s) could be found.",
|
||||
serverid,
|
||||
sub,
|
||||
)
|
||||
return False
|
||||
return CSN[0]
|
||||
|
@ -352,21 +342,19 @@ class LdapServer:
|
|||
|
||||
@staticmethod
|
||||
def get_scope(scope):
|
||||
if scope == 'base':
|
||||
if scope == "base":
|
||||
return ldap.SCOPE_BASE # pylint: disable=no-member
|
||||
if scope == 'one':
|
||||
if scope == "one":
|
||||
return ldap.SCOPE_ONELEVEL # pylint: disable=no-member
|
||||
if scope == 'sub':
|
||||
if scope == "sub":
|
||||
return ldap.SCOPE_SUBTREE # pylint: disable=no-member
|
||||
raise Exception(f'Unknown LDAP scope "{scope}"')
|
||||
raise Exception(f'Unknown LDAP scope "{scope}"') # pylint: disable=broad-exception-raised
|
||||
|
||||
def search(self, basedn, filterstr, attrs=None, scope=None):
|
||||
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(
|
||||
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 = []
|
||||
while 1:
|
||||
|
@ -380,13 +368,16 @@ class LdapServer:
|
|||
def paged_search(self, basedn, filterstr, attrs=None, scope=None):
|
||||
ret = []
|
||||
page = 0
|
||||
pg_ctrl = SimplePagedResultsControl(True, self.page_size, '')
|
||||
pg_ctrl = SimplePagedResultsControl(True, self.page_size, "")
|
||||
while page == 0 or pg_ctrl.cookie:
|
||||
page += 1
|
||||
logging.debug('Page search: loading page %d', page)
|
||||
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]
|
||||
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)
|
||||
|
@ -403,11 +394,11 @@ class LdapServer:
|
|||
if not ldif:
|
||||
return True
|
||||
try:
|
||||
logging.debug('Update object %s: %s', dn, ldif)
|
||||
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)
|
||||
logging.error("Error updating object %s", dn, exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
|
@ -432,14 +423,9 @@ class LdapServer:
|
|||
else:
|
||||
new[attr].append(TOUCH_VALUE)
|
||||
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):
|
||||
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]:
|
||||
old[attr].remove(TOUCH_VALUE)
|
||||
self.update_object(dn=dn, old=new, new=old)
|
||||
|
@ -451,8 +437,7 @@ class LdapServer:
|
|||
|
||||
if options.nocheckcert:
|
||||
# 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]
|
||||
|
||||
|
@ -461,44 +446,36 @@ 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(f'UNKWNON - Failed to connect to {srv}')
|
||||
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])
|
||||
LdapServersCSN[srv] = LdapServers[srv].getContextCSN(options.basedn, options.serverid)
|
||||
logging.info("ContextCSN of %s: %s", srv, LdapServersCSN[srv])
|
||||
|
||||
if not options.onlycheckcontextcsn:
|
||||
logging.info('List objects from %s', srv)
|
||||
logging.info("List objects from %s", srv)
|
||||
LdapObjects[srv] = {}
|
||||
|
||||
if options.attrs:
|
||||
for obj in LdapServers[srv].search(
|
||||
options.basedn, options.filterstr, []
|
||||
):
|
||||
logging.debug('Found on %s: %s', srv, obj[0][0])
|
||||
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]
|
||||
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]))
|
||||
|
||||
|
||||
if not options.onlycheckcontextcsn:
|
||||
|
@ -510,14 +487,11 @@ if not options.onlycheckcontextcsn:
|
|||
not_sync[srv] = []
|
||||
|
||||
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:
|
||||
logging.info(
|
||||
'Check if objets are synchronized (by comparing entryCSN)')
|
||||
logging.info("Check if objects are synchronized (by comparing entryCSN)")
|
||||
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():
|
||||
if srv_name == options.provider:
|
||||
continue
|
||||
|
@ -533,7 +507,9 @@ if not options.onlycheckcontextcsn:
|
|||
attrs_list.append(attr)
|
||||
logging.debug(
|
||||
"Obj %s not synchronized: %s not present on %s",
|
||||
obj, ','.join(attrs_list), srv_name
|
||||
obj,
|
||||
",".join(attrs_list),
|
||||
srv_name,
|
||||
)
|
||||
touch = True
|
||||
else:
|
||||
|
@ -543,7 +519,8 @@ if not options.onlycheckcontextcsn:
|
|||
attrs_list.append(attr)
|
||||
logging.debug(
|
||||
"Obj %s not synchronized: %s not same value(s)",
|
||||
obj, ','.join(attrs_list)
|
||||
obj,
|
||||
",".join(attrs_list),
|
||||
)
|
||||
touch = True
|
||||
if attrs_list:
|
||||
|
@ -551,29 +528,29 @@ if not options.onlycheckcontextcsn:
|
|||
else:
|
||||
logging.debug(
|
||||
"Obj %s not synchronized: %s <-> %s",
|
||||
obj, LdapObjects[options.provider][obj], srv[obj]
|
||||
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)
|
||||
LdapServers[options.provider].touch_object(obj, options.touch, orig_value)
|
||||
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)
|
||||
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)
|
||||
LdapServers[options.provider].touch_object(obj, options.touch, orig_value)
|
||||
|
||||
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]:
|
||||
logging.debug('Obj %s: not found on provider', obj)
|
||||
logging.debug("Obj %s: not found on provider", obj)
|
||||
not_found[options.provider].append(obj)
|
||||
|
||||
if options.nagios:
|
||||
|
@ -582,91 +559,75 @@ if options.nagios:
|
|||
|
||||
if not options.nocheckcontextcsn:
|
||||
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:
|
||||
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():
|
||||
if srv_name == options.provider:
|
||||
continue
|
||||
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]:
|
||||
errors.append(
|
||||
f'ContextCSN of {srv_name} not the same of provider')
|
||||
long_output.append(
|
||||
f'ContextCSN on LDAP server {srv_name} = {srv_csn}')
|
||||
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 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):')
|
||||
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}')
|
||||
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):')
|
||||
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}')
|
||||
long_output.append(f" - {obj}")
|
||||
if not_sync[options.consumer]:
|
||||
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(
|
||||
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]:
|
||||
long_output.append(f' - {obj}')
|
||||
long_output.append(f" - {obj}")
|
||||
if errors:
|
||||
print(f'CRITICAL: {", ".join(errors)}')
|
||||
print('\n\n')
|
||||
print("\n\n")
|
||||
print("\n".join(long_output))
|
||||
sys.exit(2)
|
||||
else:
|
||||
print('OK: consumer and provider are synchronized')
|
||||
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')
|
||||
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)
|
||||
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)
|
||||
logging.warning("ContextCSN of %s not the same of provider", srv_name)
|
||||
noerror = False
|
||||
|
||||
if not options.onlycheckcontextcsn:
|
||||
if not_found[srv]:
|
||||
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
|
||||
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
|
||||
|
||||
if noerror:
|
||||
logging.info('No sync problem detected')
|
||||
logging.info("No sync problem detected")
|
||||
|
|
4
debian/control
vendored
4
debian/control
vendored
|
@ -12,9 +12,9 @@ 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 fisrt, entryCSN of all entries of LDAP directory will be compare
|
||||
- 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 be compare
|
||||
- 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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
---
|
||||
name: check_syncrepl_extended
|
||||
description: "file:///README.md"
|
||||
url: "https://gitea.zionetrix.net/bn8/check_syncrepl_extended"
|
||||
|
@ -7,21 +8,17 @@ target: Database
|
|||
type: Plugin
|
||||
license: gplv2
|
||||
releases:
|
||||
-
|
||||
name: v2017.09.11
|
||||
- name: v2017.09.11
|
||||
description: "v2017.09.11 Release"
|
||||
files:
|
||||
-
|
||||
name: check_syncrepl_extended
|
||||
- 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
|
||||
- name: check_syncrepl_extended
|
||||
url: "file:///check_syncrepl_extended"
|
||||
description: "Improve ContextCSN checking (thanks Orhan)"
|
||||
checksum: a954ca49855c23f2f098f4696c4c0ebd
|
||||
|
|
Loading…
Reference in a new issue