diff --git a/bin/xymon-home.py b/bin/xymon-home.py
new file mode 100755
index 0000000000000000000000000000000000000000..498e35f5f6f37bfb5d36edc1732ac94a2cd9a435
--- /dev/null
+++ b/bin/xymon-home.py
@@ -0,0 +1,282 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import pwd
+import grp
+import stat
+import glob
+import lib_path
+import lib
+import posix1e
+import dphysldap
+import pyxymon as pymon
+
+CHECK_NAME = 'permissions'
+CHECK_VERSION = 2
+LIFETIME = 30
+
+home_dir = '/export/home1/*'
+owner = []
+permission = []
+acl = []
+home_dirs = 0
+users = {}
+nis_homes = {}
+nis_shares = {}
+bad_home_directory = []
+no_ldap_user = []
+bad_nis_map = []
+no_nis_map = []
+
+
+class Home(object):
+    """
+    Holds info about a home directory
+    """
+    def __init__(self, name, path, st):
+        self.name = name
+        self.path = path
+        self.st = st
+
+    @property
+    def uid(self):
+        return self.st.st_uid
+
+    @property
+    def gid(self):
+        return self.st.st_gid
+
+    @property
+    def uname(self):
+        return pwd.getpwuid(self.uid).pw_name
+
+    @property
+    def gname(self):
+        return grp.getgrgid(self.gid).gr_name
+
+    @property
+    def filemode(self):
+        return stat.filemode(self.st.st_mode)
+
+    @property
+    def permission(self):
+        return ' '.join([self.filemode, self.path])
+
+    def __str__(self):
+        return ' '.join([self.filemode, self.uname, self.gname, self.path])
+
+
+def search_ldap():
+    ldap = dphysldap.Ldap()
+    ldap_users = dphysldap.Users(ldap, ['uid', 'uidNumber', 'gidNumber', 'homeDirectory'])
+    entries = dphysldap.Entries(ldap, ['cn', 'nisMapEntry'])
+    auto_home = 'nisMapName=auto.home,ou=automount,dc=phys,dc=ethz,dc=ch'
+
+    ldap_users.search('*')
+    for user in ldap_users:
+        users[user['uid'][0]] = user['homeDirectory'][0]
+
+    entries.search('cn: *, nisMapEntry: phd-home*', ['nisObject'], base=auto_home)
+    for entry in entries:
+        nis_homes[entry['cn'][0]] = entry['nisMapEntry'][0]
+
+    entries.search('cn: *, nisMapEntry: != phd-home*', ['nisObject'], base=auto_home)
+    for entry in entries:
+        nis_shares[entry['cn'][0]] = entry['nisMapEntry'][0]
+
+
+def check_homes(top):
+    global home_dirs
+
+    if not os.path.isdir(top):
+        return
+    for f in os.listdir(top):
+        pathname = os.path.join(top, f)
+        if not os.path.isdir(pathname):
+            continue
+        st = os.stat(pathname)
+        home = Home(f, pathname, st)
+        home_dirs += 1
+
+        if bad_owner(home):
+            owner.append(home)
+
+        if bad_permission(home):
+            permission.append(home)
+
+        if posix1e.has_extended(home.path):
+            acl.append(home)
+
+        if home.name in users:
+            if users[home.name][6:] != home.name:
+                bad_home_directory.append(': '.join([home.name, users[home.name]]))
+            del users[home.name]
+        else:
+            no_ldap_user.append(home)
+
+        if home.name in nis_homes:
+            if nis_homes[home.name].split(':', maxsplit=1)[1] != home.path:
+                bad_nis_map.append(': '.join([home.name, nis_homes[home.name]]))
+            del nis_homes[home.name]
+        else:
+            no_nis_map.append(home)
+
+
+def check_shares():
+    for user in list(users.keys()):
+        if user in nis_shares:
+            del nis_shares[user]
+            del users[user]
+
+
+def bad_owner(home):
+    if home.name == home.uname and home.name == home.gname:
+        return False
+    return True
+
+
+def bad_permission(home):
+    # d---------
+    if home.st.st_mode == stat.S_IFDIR:
+        return False
+    # drwx------
+    elif home.st.st_mode == stat.S_IFDIR | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR:
+        return False
+    return True
+
+
+def list_homes(homes):
+    for home in homes:
+        print(home)
+
+
+def run_check(xymon):
+    search_ldap()
+
+    title = 'statistics'
+    content = ''
+    content += 'ldap users:           ' + str(len(users)) + '<br/>'
+    content += 'ldap nismaps (home):  ' + str(len(nis_homes)) + '<br/>'
+    content += 'ldap nismaps (share): ' + str(len(nis_shares)) + '<br/>'
+
+    for path in glob.glob(home_dir):
+        check_homes(path)
+
+    check_shares()
+
+    content += 'home dirs:            ' + str(home_dirs) + '<br/>'
+    content += 'strange ldap users:   ' + str(len(users)) + '<br/>'
+    content += 'orphaned nis homes:   ' + str(len(nis_homes)) + '<br/>'
+    content += 'orphaned nis shares:  ' + str(len(nis_shares)) + '<br/>'
+    content += 'bad homeDirectory:    ' + str(len(bad_home_directory)) + '<br/>'
+    content += 'no user for home:     ' + str(len(no_ldap_user)) + '<br/>'
+    content += 'bad nismaps (home):   ' + str(len(bad_nis_map)) + '<br/>'
+    content += 'no nismap for home:   ' + str(len(no_nis_map)) + '<br/>'
+
+    xymon.section(title, content)
+
+    if owner:
+        title = 'bad owner or group'
+        content = 'home must be owned by the respective user and the group his user-private-group<br/><br/>'
+        for home in owner:
+            content += ''.join([str(home), '<br/>'])
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+    if permission:
+        title = 'bad permissions'
+        content = 'home permission not <code>drwx------</code> (active user) or <code>d---------</code> (blocked user)<br/><br/>'
+        for home in permission:
+            content += ''.join([str(home.permission), '<br/>'])
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+    if acl:
+        title = 'bad acls'
+        content = 'home has posix.1e extended ACLs<br/>check acls using `getfacl`, which stands for `get fucking ACL`<br/><br/>'
+        for home in acl:
+            extacl = posix1e.ACL(file=home.path)
+            content += ''.join([home.path, '<br/>'])
+            content += ''.join([str(extacl), '<br/>'])
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+    if users:
+        title = 'strange ldap users'
+        content = 'these users seem to not have a home directory on the filesystem<br/><br/>'
+        for k, v in users.items():
+            content += ': '.join([k, v]) + '<br/>'
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+    if nis_homes:
+        title = 'orphaned nis homes'
+        content = 'these nismaps do not have a home directory on the filesystem<br/><br/>'
+        for k, v in nis_homes.items():
+            content += ': '.join([k, v]) + '<br/>'
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+    if nis_shares:
+        title = 'orphaned nis shares'
+        content = 'these nismaps do not have a user in ldap<br/><br/>'
+        for k, v in nis_shares.items():
+            content += ': '.join([k, v]) + '<br/>'
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+    if bad_home_directory:
+        title = 'bad homeDirectory'
+        content = 'the homeDirectory attributes home name does not match the username'
+        for home in bad_home_directory:
+            content += ''.join([str(home), '<br/>'])
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+    if no_ldap_user:
+        title = 'no user for home'
+        content = 'home directory without a corresponding ldap user'
+        for home in no_ldap_user:
+            content += ''.join([str(home), '<br/>'])
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+    if bad_nis_map:
+        title = 'bad nismaps (home)'
+        content = 'nismap does not match home path on the filesystem'
+        for home in bad_nis_map:
+            content += ''.join([str(home), '<br/>'])
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+    if no_nis_map:
+        title = 'no nismap for home'
+        content = 'home directory without a corresponding nismap entry'
+        for home in no_nis_map:
+            content += ''.join([str(home), '<br/>'])
+        xymon.section(title, content)
+        xymon.color = pymon.STATUS_CRITICAL
+
+
+def main():
+    """Run xymon check"""
+    xymon = pymon.XymonClient(CHECK_NAME)
+    check_script = os.path.basename(__file__)
+    # The default criticity is set to 'pymon.STATUS_OK'
+    xymon.lifetime = LIFETIME
+    xymon.title('home ownership and permissions')
+
+    try:
+        run_check(xymon)
+    except Exception as e:
+        xymon.color = pymon.STATUS_WARNING
+        xymon.section('Exception', e)
+
+    xymon.footer(check_script, CHECK_VERSION)
+    xymon.send()
+
+
+if __name__ == '__main__':
+    main()
+    sys.exit(0)
diff --git a/lib/isg/pyxymon.py b/lib/isg/pyxymon.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c7da1a0bc30293e5206b82c8cbd3ab8b68ec3d4
--- /dev/null
+++ b/lib/isg/pyxymon.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+
+"""Helper class intended for creation of Xymon Extension Modules in Python.
+
+This simple Python module provides a simple helper class that aims simplify
+ the creation of Xymon Extension Modules in Python.
+"""
+
+__author__ = "Davide Madrisan <davide.madrisan.gmail.com>"
+__copyright__ = "Copyright 2017 Davide Madrisan"
+__license__ = "GPL-3.0"
+__status__ = "Stable"
+__version__ = "3"
+
+STATUS_OK = '&green'
+STATUS_WARNING = '&yellow'
+STATUS_CRITICAL = '&red'
+
+__all__ = ['XymonClient',
+           'STATUS_OK', 'STATUS_WARNING', 'STATUS_CRITICAL']
+
+from datetime import datetime
+import os
+import socket
+
+_ALL_COLORS = (STATUS_OK, STATUS_WARNING, STATUS_CRITICAL)
+"""list of all the allower colors (criticity levels)"""
+
+class XymonMessage(object):
+    """Class for rendering the Xymon messages that will be sent to the server.
+
+    Note:
+        This class is not intended to be used directly from your code.
+    """
+    def __init__(self):
+        self._message = ''
+        self._footer = None
+        self._color = STATUS_OK
+        """default criticity"""
+
+    @staticmethod
+    def _get_date():
+        """Return the current date."""
+        return datetime.now().strftime('%c')
+
+    @staticmethod
+    def _get_machine():
+        """Get the environment variable `MACHINE` exported by Xymon.
+
+        Raises:
+            RuntimeError: If `MACHINE` is not set.
+        """
+        xymon_machine = os.environ.get('MACHINE')
+        if not xymon_machine:
+            raise RuntimeError('The environment variable MACHINE is not set')
+        return xymon_machine
+
+    @property
+    def color(self):
+        """Return the current color (message criticity level)."""
+        return self._color
+
+    @color.setter
+    def color(self, value):
+        """Set the color (message criticity level) to `value`.
+
+        Note:
+            The color is not updated when `value` has a criticity
+            lower than the current one `self._color`.
+
+        Attributes:
+            value (str): The new color to be set.
+                         The following colors are the only valid ones:
+                           - pyxymon.STATUS_OK
+                           - pyxymon.STATUS_WARNING
+                           - pyxymon.STATUS_CRITICAL
+        Raises:
+            ValueError: If `value` is not a valid color string.
+        """
+        if value not in _ALL_COLORS:
+            raise ValueError('Illegal color for xymon: {0}'.format(value))
+        current_color_index = _ALL_COLORS.index(self._color)
+        new_color_index = _ALL_COLORS.index(value)
+        if new_color_index > current_color_index:
+            self._color = value
+
+    def title(self, text):
+        """Set the message title.
+
+        Attributes:
+            text (str): The string containing the title.
+        """
+        self._message += '<br><h1>{0}</h1><hr><br>'.format(text)
+
+    def section(self, title, body):
+        """Add a section to the Xymon message.
+
+        Attributes:
+            title (str): The string containing the title of this section.
+            body (str): The content of the section.
+        """
+        self._message += (
+            '<h2>{0}</h2><p>{1}</p><br>'.format(title, body))
+
+    def footer(self, check_filename, check_version):
+        """Add a footer the the Xymon message.
+
+        Attributes:
+            check_filename (str): The name of the check script.
+            check_version (str): The version of the check script.
+        """
+        self._footer = (
+            '<br>'
+            '<center>xymon script: {0} version {1}</center>'.format(
+                check_filename, check_version))
+
+    def _render(self, test):
+        """Return the message string in a format accepted by the Xymon server.
+
+        Attributes:
+            test (str): The string containing the name of the Xymon test.
+
+        Raises:
+            RuntimeError: If `self._color` is an illegal color
+                          (this should never happen).
+        """
+        date = self._get_date()
+        machine = self._get_machine()
+        if self._color not in _ALL_COLORS:
+            raise RuntimeError(
+                'Illegal color for xymon: {0}'.format(self._color))
+        html = (self._message if not self._footer else
+                self._message + self._footer)
+        return 'status {0}.{1} {2} {3}\n{4}\n'.format(
+            machine, test, self._color[1:], date, html)
+
+class XymonClient(XymonMessage):
+    """Class for managing and sending the final message to the Xymon server.
+
+    Attributes:
+        test (str): Name of the Xymon test.
+
+    Usage:
+        import os
+        import pyxymon as pymon
+        check_name = 'mytest'
+        check_version = 1
+        check_filename = os.path.basename(__file__)
+        xymon = pymon.XymonClient(check_name)
+        # do your logic...
+        # you can set the criticity of the final xymon message by using:
+        #    xymon.color = pymon.STATUS_WARNING
+        # or
+        #    xymon.color = pymon.STATUS_CRITICAL
+        # The criticity is set by default to 'pymon.STATUS_OK'
+        xymon.title('Title in the xymon check page')
+        xymon.section('Section Title',
+                      'Text containing the lines you want to display')
+        # You can add here other sections, if required.
+        xymon.footer(check_filename, check_version)
+        xymon.send()
+    """
+    def __init__(self, test):
+        XymonMessage.__init__(self)
+        self.test = test
+        """Name of the Xymon test"""
+
+    @staticmethod
+    def _get_xymon_server_name():
+        """Return the content of the environment variable XYMSRV.
+
+        Raises:
+            RuntimeError: If `XYMSRV` is not set.
+        """
+        xymon_server = os.environ.get('XYMSRV')
+        if not xymon_server:
+            RuntimeError('The environment variable XYMSRV is not set')
+        return os.environ.get('XYMSRV')
+
+    @staticmethod
+    def _get_xymon_server_port():
+        """Return the content of the environment variable XYMONDPORT.
+
+        Note:
+            The default Xymon port (1984) is returned, when such a variable
+            does not exist.
+        """
+        xymon_port = os.environ.get('XYMONDPORT', 1984)
+        return int(xymon_port)
+
+    def send(self):
+        """Send a rendered message to the xymon server.
+
+        Note:
+            The server and port are read from the environment variables
+            XYMSRV and XYMONDPORT (default set to 1984 when not found).
+        """
+        server = self._get_xymon_server_name()
+        port = self._get_xymon_server_port()
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.connect((server, port))
+        xymon_string = self._render(self.test)
+        sock.send(xymon_string.encode('utf-8'))
+        sock.close()