Compare commits

..

No commits in common. "b1953a5b5115893d9fb7fdfea452fa5be94fcab7" and "a3d6c8cfb086ce0a10f2b0b03da568b1e5ebaca7" have entirely different histories.

12 changed files with 81 additions and 327 deletions

1
.gitignore vendored
View file

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

View file

@ -1,71 +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
- pylint3 gitdch
test-flake8:
group: test
image: pipelinecomponents/flake8
commands:
- flake8 gitdch
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
files:
- dist/check-syncrepl-extended-*/check_syncrepl_extended
- dist/*.deb
checksum:
- md5
- sha512
publish-apt:
group: publish
image: brenard/curl
when:
event: tag
commands:
- curl -u $APT_CREDS -X POST -F file=@$( ls dist/check-syncrepl-extended_*_all.deb ) $APT_API_URL/files/gitdch
- curl -u $APT_CREDS -X POST -F file=@$( ls dist/check-syncrepl-extended_*.buildinfo ) $APT_API_URL/files/gitdch
- curl -u $APT_CREDS -X POST -F file=@$( ls dist/check-syncrepl-extended_*.changes ) $APT_API_URL/files/gitdch
- curl -u $APT_CREDS -X POST -F file=@$( ls dist/check-syncrepl-extended_*.dsc ) $APT_API_URL/files/gitdch
- curl -u $APT_CREDS -X POST -F file=@$( ls dist/check-syncrepl-extended_*.tar.gz ) $APT_API_URL/files/gitdch
- curl -u $APT_CREDS -X POST $APT_API_URL/repos/$APT_REPO_NAME/include/gitdch
- APT_SNAP_NAME=$(date +%s)_$APT_REPO_NAME
- >
curl -u $APT_CREDS -X POST -H 'Content-Type: application/json' --data "{\"Name\":\"$APT_SNAP_NAME\"}" $APT_API_URL/repos/$APT_REPO_NAME/snapshots
- >
curl -u $APT_CREDS -X PUT -H 'Content-Type: application/json' --data "{\"Snapshots\": [{\"Component\": \"main\", \"Name\": \"$APT_SNAP_NAME\"}]}" $APT_API_URL/publish/:./$APT_REPO_NAME
secrets: [ apt_api_url, apt_creds, apt_repo_name ]

View file

@ -1,61 +0,0 @@
#!/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 "Retreive 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 \
"${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

@ -43,19 +43,15 @@ import getpass
import ldap
from ldap import LDAPError # pylint: disable=no-name-in-module
from ldap.controls import SimplePagedResultsControl
from ldap import modlist
import ldap.modlist as modlist
VERSION = '0.0'
TOUCH_VALUE = '%%TOUCH%%'
parser = argparse.ArgumentParser(
description=(
"Script to check LDAP syncrepl replication state between "
description=("Script to check LDAP syncrepl replication state between "+
"two servers."),
epilog=(
'Author: Benjamin Renard <brenard@easter-eggs.com>, '
f'Version: {VERSION}, '
'Source: https://gogs.zionetrix.net/bn8/check_syncrepl_extended')
epilog=("Author: Benjamin Renard <brenard@easter-eggs.com>, "+
"Source: https://gogs.zionetrix.net/bn8/check_syncrepl_extended")
)
parser.add_argument(
@ -79,11 +75,10 @@ parser.add_argument(
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' "
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
)
@ -174,8 +169,7 @@ parser.add_argument(
"--only-check-contextCSN",
dest="onlycheckcontextcsn",
action="store_true",
help=(
"Only check servers root contextCSN (objects check disabled, "
help=("Only check servers root contextCSN (objects check disabled, "+
"default : False)"),
default=False
)
@ -202,13 +196,11 @@ parser.add_argument(
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}" 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.'
),
help=("Touch attribute giving in parameter to force resync a this LDAP "+
"object from provider. A value '{}' 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."
).format(TOUCH_VALUE),
default=None
)
@ -233,8 +225,7 @@ parser.add_argument(
dest="page_size",
action="store",
type=int,
help=(
"Page size: if defined, paging control using LDAP v3 extended "
help=("Page size: if defined, paging control using LDAP v3 extended " +
"control will be enabled."),
default=None
)
@ -242,8 +233,7 @@ parser.add_argument(
options = parser.parse_args()
if options.nocheckcontextcsn and options.onlycheckcontextcsn:
parser.error(
"You can't use both --no-check-contextCSN and "
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)
@ -261,10 +251,8 @@ if not options.basedn:
sys.exit(3)
sys.exit(1)
if not 0 <= options.serverid <= 4095:
parser.error(
"ServerID should be a integer value from 0 to 4095 "
parser.error("ServerID should be a integer value from 0 to 4095 "+
"(limited to 3 hexadecimal digits).")
if options.nagios:
sys.exit(3)
@ -294,8 +282,7 @@ elif options.quiet:
else:
logging.basicConfig(level=logging.INFO, format=FORMAT)
class LdapServer:
class LdapServer(object):
uri = ""
dn = ""
@ -315,8 +302,7 @@ class LdapServer:
if self.con == 0:
try:
con = ldap.initialize(self.uri)
# pylint: disable=no-member
con.protocol_version = ldap.VERSION3
con.protocol_version = ldap.VERSION3 # pylint: disable=no-member
if self.start_tls:
con.start_tls_s()
if self.dn:
@ -330,20 +316,18 @@ 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)
if serverid is False:
return contextCSNs[0]
csnid = str(format(serverid, 'X')).zfill(3)
sub = str.encode(f'#{csnid}#', encoding="ascii", errors="replace")
sub = str.encode('#%s#' % 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.",
"No contextCSN matching with ServerID %s (=%s) could be found.",
serverid, sub
)
return False
@ -358,12 +342,11 @@ class LdapServer:
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}"')
raise Exception("Unknown LDAP scope '%s'" % scope)
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 []
@ -388,8 +371,7 @@ class LdapServer:
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)
res_type, res_data, res_id, serverctrls = self.con.result3(res_id) # pylint: disable=unused-variable
for serverctrl in serverctrls:
if serverctrl.controlType == SimplePagedResultsControl.controlType:
pg_ctrl.cookie = serverctrl.cookie
@ -400,7 +382,7 @@ class LdapServer:
def update_object(self, dn, old, new):
ldif = modlist.modifyModlist(old, new)
if not ldif:
if ldif == []:
return True
try:
logging.debug('Update object %s: %s', dn, ldif)
@ -437,9 +419,7 @@ class LdapServer:
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)
@ -448,11 +428,8 @@ class LdapServer:
logging.error('Error touching object "%s"', dn, exc_info=True)
return False
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) # pylint: disable=no-member
servers = [options.provider, options.consumer]
@ -468,14 +445,13 @@ for srv in servers:
if not LdapServers[srv].connect():
if options.nagios:
print(f'UNKWNON - Failed to connect to {srv}')
print("UNKWNON - Failed to connect to %s" % srv) # pylint: disable=print-statement
sys.exit(3)
else:
sys.exit(1)
if not options.nocheckcontextcsn:
LdapServersCSN[srv] = LdapServers[srv].getContextCSN(
options.basedn, options.serverid)
LdapServersCSN[srv] = LdapServers[srv].getContextCSN(options.basedn, options.serverid)
logging.info('ContextCSN of %s: %s', srv, LdapServersCSN[srv])
if not options.onlycheckcontextcsn:
@ -483,15 +459,11 @@ for srv in servers:
LdapObjects[srv] = {}
if options.attrs:
for obj in LdapServers[srv].search(
options.basedn, options.filterstr, []
):
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']
):
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]
@ -510,36 +482,33 @@ 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 objets 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:
for srv in LdapObjects:
if srv == options.provider:
continue
if obj in srv:
if obj in LdapObjects[srv]:
touch = False
if LdapObjects[options.provider][obj] != srv[obj]:
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 srv[obj]:
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_name
obj, ','.join(attrs_list), srv
)
touch = True
else:
srv[obj][attr].sort()
LdapObjects[srv][obj][attr].sort()
LdapObjects[options.provider][obj][attr].sort()
if srv[obj][attr] != LdapObjects[options.provider][obj][attr]:
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)",
@ -547,28 +516,26 @@ if not options.onlycheckcontextcsn:
)
touch = True
if attrs_list:
not_sync[srv_name].append(f'{obj} ({",".join(attrs_list)})')
not_sync[srv].append("%s (%s)" % (obj, ','.join(attrs_list)))
else:
logging.debug(
"Obj %s not synchronized: %s <-> %s",
obj, LdapObjects[options.provider][obj], srv[obj]
obj, LdapObjects[options.provider][obj], LdapObjects[srv][obj]
)
not_sync[srv_name].append(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)
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)
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)
LdapServers[options.provider].touch_object(obj, options.touch, orig_value)
for obj in LdapObjects[options.consumer]:
logging.debug('Check obj %s of consumer', obj)
@ -584,74 +551,54 @@ if options.nagios:
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:
long_output.append('ContextCSN on LDAP server provider = %s' % LdapServersCSN[options.provider])
for srv in LdapServersCSN:
if srv == 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 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)
long_output.append('ContextCSN on LDAP server %s = %s' % (srv, LdapServersCSN[srv]))
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("%s not found object(s) on consumer" % len(not_found[options.consumer]))
long_output.append("Object(s) not found on server %s (consumer) :" % options.consumer)
for obj in not_found[options.consumer]:
long_output.append(f' - {obj}')
long_output.append(" - %s" % 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("%s not found object(s) on provider" % len(not_found[options.provider]))
long_output.append("Object(s) not found on server %s (provider) :" % options.provider)
for obj in not_found[options.provider]:
long_output.append(f' - {obj}')
long_output.append(" - %s" % 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):')
errors.append("%s not synchronized object(s) on consumer" % len(not_sync[options.consumer]))
long_output.append("Object(s) not synchronized on server %s (consumer) :" % options.consumer)
for obj in not_sync[options.consumer]:
long_output.append(f' - {obj}')
long_output.append(" - %s" % obj)
if errors:
print(f'CRITICAL: {", ".join(errors)}')
print('\n\n')
print("\n".join(long_output))
print("CRITICAL: " + ', '.join(errors) + "\n\n" + "\n".join(long_output)) # pylint: disable=print-statement
sys.exit(2)
else:
print('OK: consumer and provider are synchronized')
print('OK: consumer and provider are synchronized') # pylint: disable=print-statement
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:
for srv in LdapServersCSN:
if srv == options.provider:
continue
if not srv_csn:
logging.warning('ContextCSN of %s not found', srv_name)
if not LdapServersCSN[srv]:
logging.warning('ContextCSN of %s not found', srv)
noerror = False
elif srv_csn != LdapServersCSN[options.provider]:
logging.warning(
'ContextCSN of %s not the same of provider',
srv_name)
elif LdapServersCSN[srv] != LdapServersCSN[options.provider]:
logging.warning('ContextCSN of %s not the same of provider', srv)
noerror = False
if not options.onlycheckcontextcsn:

1
debian/compat vendored
View file

@ -1 +0,0 @@
11

30
debian/control vendored
View file

@ -1,30 +0,0 @@
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 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.
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
View file

@ -1,20 +0,0 @@
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
View file

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

1
debian/install vendored
View file

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

4
debian/rules vendored
View file

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

View file

@ -1 +0,0 @@
1.0

View file

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