esme.py 17.5 KB
Newer Older
1 2 3 4 5 6 7 8 9
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Nov 24 15:35:04 2017

@author: Lorenz Steinert
"""

import os
Lorenz Steinert's avatar
Lorenz Steinert committed
10 11 12
import re
import time
import codecs
Lorenz Steinert's avatar
Lorenz Steinert committed
13
import logging
14
import configparser
15
from enum import Flag, auto
16 17 18 19 20 21 22 23 24
import ldap3 as ldap

try:
    import pwd
except ImportError:
    import getpass
    pwd = None


25
class Error(Flag):
Lorenz Steinert's avatar
Lorenz Steinert committed
26 27 28
    """
    Error constants for the FsrLdap class
    """
29 30 31
    UNSEC_SEARCH = auto()
    NO_FILE = auto()
    LDAP_FAIL = auto()
32
    NO_DEFAULT_FILE = auto()
Lorenz Steinert's avatar
Lorenz Steinert committed
33 34
    NO_DIR = auto()
    NO_DEFAULT_DIR = auto()
35

36

37
def current_user():
Lorenz Steinert's avatar
Lorenz Steinert committed
38 39 40
    """
    get the uid of the current user
    """
41 42 43 44 45
    if pwd:
        return pwd.getpwuid(os.geteuid()).pw_name
    return getpass.getuser()


46
def resort_fsr(fsr, num_cols=5):
Lorenz Steinert's avatar
Lorenz Steinert committed
47 48 49
    """
    resort list of names by pascal style
    """
50 51 52 53 54 55 56 57 58 59 60
    fsr += [""] * (num_cols - len(fsr) % num_cols)
    depth = len(fsr) // num_cols

    tmp = [[] for i in range(depth)]

    for index, elem in zip(range(len(fsr)), fsr):
        tmp[index % depth] += [elem]

    return [j for i in tmp for j in i]


61
class FsrLdap:
Lorenz Steinert's avatar
Lorenz Steinert committed
62 63 64
    """
    Class providing LDAP integration
    """
65

Lorenz Steinert's avatar
Lorenz Steinert committed
66 67
    def __init__(self, server, base, share_dir, config, debug=False,
                 timeout=1, testing=None):
68 69
        self.debug = debug

70
        self.server = ldap.Server(server,
71
                                  connect_timeout=timeout)
72 73 74
        if not testing:
            self.conn = ldap.Connection(self.server)
        else:
Lorenz Steinert's avatar
Lorenz Steinert committed
75 76
            self.conn = ldap.Connection(self.server,
                                        client_strategy=ldap.MOCK_SYNC)
77 78 79 80 81 82

        try:
            self.conn.bind()
        except ldap.core.exceptions.LDAPSocketOpenError:
            pass

83
        self.base = base
84
        self.share_dir = share_dir
85
        self.config = config
86

87
    def _get_protokollant_ldap(self):
Lorenz Steinert's avatar
Lorenz Steinert committed
88 89 90 91 92
        """
        get the name of the current user by searching the LDAP-Server
        if the search is unsuccessful return Error.UNSEC_SEARCH
        if the search fails because of a LDAPException return Error.LDAP_FAIL
        """
Lorenz Steinert's avatar
Lorenz Steinert committed
93
        logging.info("Fetching Protokollant from LDAP ...")
94
        try:
Lorenz Steinert's avatar
Lorenz Steinert committed
95 96
            fil = "(objectClass=posixAccount)"
            if self.conn.search('uid=' + current_user() + ',ou=people,' + self.base, fil,  # noqa: E501
97
                                attributes=['displayName']):
Lorenz Steinert's avatar
Lorenz Steinert committed
98
                logging.info("Done")
Lorenz Steinert's avatar
Lorenz Steinert committed
99
                return " ".join(str(self.conn.entries[0]['displayName']).split(", ")[::-1])  # noqa: E501
100
            return Error.UNSEC_SEARCH
101 102
        except ldap.core.exceptions.LDAPException as error_message:
            if self.debug:
Lorenz Steinert's avatar
Lorenz Steinert committed
103 104
                logging.warning("Couldn't connect to the LDAP-Server. \
                                Using fall-back default. %s" % error_message)
105
            else:
Lorenz Steinert's avatar
Lorenz Steinert committed
106
                logging.warning("Couldn't Connect to the LDAP-Server. Using fall-back default.")  # noqa: E501
107 108 109 110
            return Error.LDAP_FAIL

    @staticmethod
    def _get_fallback_protokollant(config=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
111 112 113 114 115 116
        """
        get the name of the Protokollant from user input or config
        The values are used in this order:
        - the Protokollant provided by the user
        - config['PROTOKOLL']['Protokollant']
        """
117 118 119 120 121 122 123 124 125
        protokollant = ''
        if config:
            protokollant = config['PROTOKOLL']['Protokollant']
        ptmp = input("Protokollant ["+protokollant+"]: ")
        if ptmp != "":
            return ptmp
        return protokollant

    def get_protokollant(self, config=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
126 127 128 129 130 131 132 133 134
        """
        get the Name of the Protokollant
        The Name sources are used in this order:
        - LDAP-Server
        - user input
        - config

        if non is found return an empty string
        """
135 136 137 138
        if not config:
            config = self.config

        protokollant = self._get_protokollant_ldap()
Lorenz Steinert's avatar
Lorenz Steinert committed
139
        if isinstance(protokollant, tuple):
140
            protokollant = protokollant[0]
141 142 143 144 145
        elif isinstance(protokollant, Flag):
            if protokollant in Error.UNSEC_SEARCH | Error.LDAP_FAIL:
                protokollant = self._get_fallback_protokollant(config)
            if protokollant == Error.NO_FILE:
                protokollant = ""
146
        return protokollant
147

148
    def _get_fsr_ldap(self):
Lorenz Steinert's avatar
Lorenz Steinert committed
149 150 151 152 153 154
        """
        get the list of the names of the FSR by searching the LDAP-Server

        if the search is unsuccessful return Error.UNSEC_SEARCH
        if the search fails because of a LDAPException return Error.LDAP_FAIL
        """
Lorenz Steinert's avatar
Lorenz Steinert committed
155
        logging.info("Fetching FSR from LDAP ...")
156 157
        try:
            fil = '(cn=intern)'
Lorenz Steinert's avatar
Lorenz Steinert committed
158
            fsr = []
159
            if self.conn.search('ou=group,'+self.base, fil,
Lorenz Steinert's avatar
Lorenz Steinert committed
160 161 162
                                attributes=['member']):
                for i in self.conn.entries[0]['member']:
                    if self.conn.search(i, '(objectClass=posixAccount)',
Lorenz Steinert's avatar
Lorenz Steinert committed
163
                                        attributes=['displayName']):
164
                        fsr += [str(self.conn.entries[0]['displayName'])]
Lorenz Steinert's avatar
Lorenz Steinert committed
165 166
                fsr.sort()
                fsr = [" ".join(i.split(', ')[::-1]) for i in fsr]
Lorenz Steinert's avatar
Lorenz Steinert committed
167
                logging.info("Done")
Lorenz Steinert's avatar
Lorenz Steinert committed
168
                return fsr
169
            return Error.UNSEC_SEARCH
170 171
        except ldap.core.exceptions.LDAPException as error_message:
            if self.debug:
Lorenz Steinert's avatar
Lorenz Steinert committed
172 173
                logging.warning("Couldn't Connect to the LDAP-Server. \
                                Using the fall-back file. %s" % error_message)
174
            else:
Lorenz Steinert's avatar
Lorenz Steinert committed
175
                logging.warning("Couldn't Connect to the LDAP-Server. Using fall-back file.")  # noqa: E501
176 177 178 179
            return (Error.LDAP_FAIL, error_message)

    @staticmethod
    def _get_fallback_fsr(share_dir):
Lorenz Steinert's avatar
Lorenz Steinert committed
180 181 182 183 184
        """
        get the list of the names of the FSR from the fall back file

        if the file is not found return Error.NO_FILE
        """
185
        if os.path.isfile(os.path.join(share_dir, 'fsr')):
Lorenz Steinert's avatar
Lorenz Steinert committed
186 187
            with codecs.open(os.path.join(share_dir, 'fsr'),
                             'r', 'utf-8') as fobj:
188 189
                fsr = sorted([re.sub(re.escape('\t'), ' ',
                                     line.rstrip('\n'))
190 191
                              for line in fobj.readlines()
                              if line.rstrip('\n')],
192 193 194 195 196
                             key=lambda x: ''.join(x.split(' ')[::-1]))
            return fsr
        return Error.NO_FILE

    def get_fsr(self, share_dir=None, gen_fallback=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
197 198 199 200 201 202 203 204 205
        """
        get a sorted list of the names of the FSR
        The name sources are used in this order:
        - LDAP-Server
        - user input
        - fall-back file

        if non is found return an empty list
        """
206 207 208 209
        if not share_dir:
            share_dir = self.share_dir

        fsr = self._get_fsr_ldap()
Lorenz Steinert's avatar
Lorenz Steinert committed
210
        if isinstance(fsr, tuple):
211
            fsr = fsr[0]
212
        if isinstance(fsr, Error):
Lorenz Steinert's avatar
Lorenz Steinert committed
213 214
            if fsr & (Error.UNSEC_SEARCH | Error.LDAP_FAIL) \
                    and not gen_fallback:
215
                fsr = self._get_fallback_fsr(share_dir)
216 217
        if isinstance(fsr, Error):
            if fsr & (Error.NO_FILE | Error.UNSEC_SEARCH):
218
                fsr = []
219
        return resort_fsr(fsr, num_cols=self.config['PROTOKOLL'].getint('NUM_COLS'))  # noqa: E501
220

221
    def _get_fsr_extern_ldap(self):
Lorenz Steinert's avatar
Lorenz Steinert committed
222 223 224 225 226 227 228 229
        """
        get the list of the names of the eFSR
        by searching the LDAP-Server

        if the search is unsuccessful return Error.UNSEC_SEARCH
        if the search fails because of a LDAPException return Error.LDAP_FAIL
        """
        logging.info("Fetching eFSR from LDAP ...")
230 231
        try:
            fil = '(cn=extern)'
Lorenz Steinert's avatar
Lorenz Steinert committed
232
            fsr_extern = []
233
            if self.conn.search('ou=group,'+self.base, fil,
Lorenz Steinert's avatar
Lorenz Steinert committed
234 235 236
                                attributes=['member']):
                for i in self.conn.entries[0]['member']:
                    if self.conn.search(i, '(objectClass=posixAccount)',
Lorenz Steinert's avatar
Lorenz Steinert committed
237 238
                                        attributes=['displayName']):
                        fsr_extern += [str(self.conn.entries[0]['displayName'])]  # noqa: E501
Lorenz Steinert's avatar
Lorenz Steinert committed
239
                fsr_extern.sort()
Lorenz Steinert's avatar
Lorenz Steinert committed
240
                fsr_extern = [" ".join(i.split(', ')[::-1]) for i in fsr_extern]  # noqa: E501
Lorenz Steinert's avatar
Lorenz Steinert committed
241
                logging.info("Done")
Lorenz Steinert's avatar
Lorenz Steinert committed
242
                return fsr_extern
243
            return Error.UNSEC_SEARCH
244 245
        except ldap.core.exceptions.LDAPException as error_message:
            if self.debug:
Lorenz Steinert's avatar
Lorenz Steinert committed
246 247
                logging.warning("Couldn't Connect to ldap. \
                                Using fallback File. %s" % error_message)
248
            else:
Lorenz Steinert's avatar
Lorenz Steinert committed
249
                logging.warning("Couldn't connect to the LDAP-Server. Using fall-back file.")  # noqa: E501
250 251 252 253
            return (Error.LDAP_FAIL, error_message)

    @staticmethod
    def _get_fallback_fsr_extern(share_dir):
Lorenz Steinert's avatar
Lorenz Steinert committed
254 255 256 257
        """
        get the list of the names of the extern FSR from the fall back file
        if the file is not found return Error.NO_FILE
        """
258
        if os.path.isfile(os.path.join(share_dir, 'fsr_extern')):
Lorenz Steinert's avatar
Lorenz Steinert committed
259 260
            with codecs.open(os.path.join(share_dir, 'fsr_extern'),
                             'r', 'utf-8') as fobj:
261 262
                fsr_extern = sorted([re.sub(re.escape('\t'), ' ',
                                            line.rstrip('\n'))
263 264
                                     for line in fobj.readlines()
                                     if line.rstrip('\n')],
265 266 267 268 269
                                    key=lambda x: ''.join(x.split(' ')[::-1]))
            return fsr_extern
        return Error.NO_FILE

    def get_fsr_extern(self, share_dir=None, gen_fallback=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
270 271 272 273 274 275 276 277
        """
        get a sorted list of the names of the eFSR
        The name sources are used in this order:
        - LDAP-Server
        - fall-back file

        if non is found return an empty list
        """
278 279 280
        if not share_dir:
            share_dir = self.share_dir

281 282
        fsr_extern = self._get_fsr_extern_ldap()
        if isinstance(fsr_extern, tuple):
283
            pass
284
            fsr_extern = fsr_extern[0]
285
        if isinstance(fsr_extern, Error):
Lorenz Steinert's avatar
Lorenz Steinert committed
286 287
            if fsr_extern & (Error.UNSEC_SEARCH | Error.LDAP_FAIL) \
                    and not gen_fallback:
288
                fsr_extern = self._get_fallback_fsr_extern(share_dir)
289 290
        if isinstance(fsr_extern, Error):
            if fsr_extern & (Error.NO_FILE | Error.UNSEC_SEARCH):
291
                fsr_extern = []
292
        return resort_fsr(fsr_extern, num_cols=self.config['PROTOKOLL'].getint('NUM_COLS_EXT'))  # noqa: E501
293

294

295
def get_sprecher(config=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
296 297 298 299 300 301
    """
    get Sprecher from user input or config
    The values are used in this order:
    - the Sprecher provided by the user
    - config['PROTOKOLL']['Sprecher']
    """
302
    sprecher = ''
303
    if config:
304
        sprecher = config['PROTOKOLL']['Sprecher']
305
    stmp = input("Sprecher ["+sprecher+"]: ")
306 307 308 309
    if stmp != "":
        return stmp
    return sprecher

310

311
def get_date(test=False):
Lorenz Steinert's avatar
Lorenz Steinert committed
312 313 314 315 316 317 318
    """
    get current date then ask user
    The dates are used in this order:
    - if test is true use the 01.01.2000
    - the date provided by the user
    - the current system time
    """
Lorenz Steinert's avatar
Lorenz Steinert committed
319 320 321 322 323 324 325 326 327 328 329 330
    if test:
        date = ('01.01.2000', '2000', '01', '01')
    else:
        year = str(time.localtime()[0])
        mon = str(time.localtime()[1])
        if len(mon) < 2:
            mon = "0"+mon
        day = str(time.localtime()[2])
        if len(day) < 2:
            day = "0"+day
        date = (day + '.' + mon + '.' + year, year, mon, day)

331
    dtmp = input("Datum ["+date[0]+"]: ")
332 333
    if dtmp != "":
        day, mon, year = dtmp.split('.')
334 335 336 337
        if len(mon) < 2:
            mon = "0" + mon
        if len(day) < 2:
            day = "0" + day
338 339
        date = day + "." + mon + "." + year
        return (date, year, mon, day)
Lorenz Steinert's avatar
Lorenz Steinert committed
340

341 342
    return date

343

344
def get_editor(editor):
Lorenz Steinert's avatar
Lorenz Steinert committed
345 346 347
    """
    get the editor to use from user input
    """
348
    etmp = input("Editor ["+editor+"]: ")
349 350 351
    if etmp != "":
        return etmp
    return editor
352 353


Lorenz Steinert's avatar
Lorenz Steinert committed
354
def get_config(rundir, config_path=None, default=False):
Lorenz Steinert's avatar
Lorenz Steinert committed
355 356 357 358 359 360 361 362 363 364 365 366
    """
    get the location of the config file and load configparser
    The files are used in this order:
    - `config_path`
    - if default is specified:
      - `rundir/../etc/proto.ini.default`
    - `rundir/../etc/proto.ini`
    - `rundir/../etc/proto.ini.default`

    if the file given by config_path is not found return Error.NO_FILE
    if the default file is not found return Error.NO_DEFAULT_FILE
    """
367 368 369 370 371
    if config_path:
        if os.path.isfile(config_path):
            config = configparser.ConfigParser()
            config.read(config_path)
            return config
Lorenz Steinert's avatar
Lorenz Steinert committed
372
        logging.warning(config_path + ': Not a file')
373
        return Error.NO_FILE
Lorenz Steinert's avatar
Lorenz Steinert committed
374 375 376 377 378
    elif default:
        if os.path.isfile(os.path.join(rundir, '../etc/proto.ini.default')):
            config = configparser.ConfigParser()
            config.read(os.path.join(rundir, '../etc/proto.ini.default'))
            return config
Lorenz Steinert's avatar
Lorenz Steinert committed
379
        logging.warning('No default File found at '
380
                        + os.path.abspath(os.path.join(rundir,
Lorenz Steinert's avatar
Lorenz Steinert committed
381
                                                       '../etc/proto.ini.default')))  # noqa: E501
382
        return Error.NO_DEFAULT_FILE
Lorenz Steinert's avatar
Lorenz Steinert committed
383 384
    elif os.path.isfile(os.path.join(rundir,
                                     '../etc/proto.ini')) and not default:
385 386 387
        config = configparser.ConfigParser()
        config.read(os.path.join(rundir, '../etc/proto.ini'))
        return config
388
    elif os.path.isfile(os.path.join(rundir, '../etc/proto.ini.default')):
Lorenz Steinert's avatar
Lorenz Steinert committed
389
        logging.warning('No non default ini file found using default file.')
390 391 392
        config = configparser.ConfigParser()
        config.read(os.path.join(rundir, '../etc/proto.ini.default'))
        return config
Lorenz Steinert's avatar
Lorenz Steinert committed
393
    logging.warning('No ini file found. Using hard codded defaults.')
394
    return Error.NO_DEFAULT_FILE
395 396 397


def get_share(rundir, config=None, sharedir=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
398 399 400 401 402 403 404 405 406 407 408 409
    """
    get the location of the share directory
    The directories are used in this order:
    - `sharedir`
    - the one provided in config['PROTOKOLL']['share']
        - if the path is a relative one it is relative
          to the script directory
    - `rundir/../share`

    if a provided directory is not found return Error.NO_FILE
    if the default directory is not found return Error.NO_DEFAULT_FILE
    """
410 411 412
    if sharedir:
        if os.path.isdir(sharedir):
            return sharedir
Lorenz Steinert's avatar
Lorenz Steinert committed
413
        logging.warning(sharedir + ': Not a Directory')
Lorenz Steinert's avatar
Lorenz Steinert committed
414
        return Error.NO_DIR
415
    elif config:
416 417
        config_share = config['PROTOKOLL']['share']
        if os.path.isdir(config_share) and os.path.isabs(config_share):
418
            return config['PROTOKOLL']['share']
419 420 421
        elif os.path.isdir(os.path.join(rundir, '..', config_share)):
            return os.path.normpath(os.path.join(rundir, '..', config_share))
        logging.warning(config_share + ': Not a Directory')
Lorenz Steinert's avatar
Lorenz Steinert committed
422
        return Error.NO_DIR
423
    elif os.path.isdir(os.path.join(rundir, '../share')):
Lorenz Steinert's avatar
Lorenz Steinert committed
424
        return os.path.normpath(os.path.join(rundir, '../share'))
Lorenz Steinert's avatar
Lorenz Steinert committed
425
    logging.warning('No share Directory found at '
426
                    + os.path.abspath(os.path.join(rundir, '../share')))
Lorenz Steinert's avatar
Lorenz Steinert committed
427
    return Error.NO_DEFAULT_DIR
428

Lorenz Steinert's avatar
Lorenz Steinert committed
429

430
def get_path(rundir, config=None, path=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
431 432 433 434 435 436 437 438 439 440 441 442
    """
    get the location of the Protokoll directory
    The directories are used in this order:
    - `path`
    - the one provided in config['PROTOKOLL']['path']
        - if the path is a relative one it is relative
          to the script directory
    - `rundir/../..`

    if a provided directory is not found return Error.NO_FILE
    if the default directory is not found return Error.NO_DEFAULT_FILE
    """
443 444 445
    if path:
        if os.path.isdir(path):
            return path
Lorenz Steinert's avatar
Lorenz Steinert committed
446
        logging.warning(path + ': Not a Directory')
Lorenz Steinert's avatar
Lorenz Steinert committed
447
        return Error.NO_DIR
448
    elif config:
449 450 451 452 453
        config_path = config['PROTOKOLL']['path']
        if os.path.isdir(config_path) and os.path.isabs(config_path):
            return config_path
        elif os.path.isdir(os.path.join(rundir, '..', config_path)):
            return os.path.normpath(os.path.join(rundir, '..', config_path))
Lorenz Steinert's avatar
Lorenz Steinert committed
454
        logging.warning(config['PROTOKOLL']['path'] + ': Not a Directory')
Lorenz Steinert's avatar
Lorenz Steinert committed
455
        return Error.NO_DIR
456
    elif os.path.isdir(os.path.join(rundir, '../..')):
Lorenz Steinert's avatar
Lorenz Steinert committed
457
        return os.path.normpath(os.path.join(rundir, '../..'))
Lorenz Steinert's avatar
Lorenz Steinert committed
458
    logging.warning('No Protokoll Directory found at '
Lorenz Steinert's avatar
Lorenz Steinert committed
459
                    + os.path.normpath(os.path.join(rundir, '../..')))
Lorenz Steinert's avatar
Lorenz Steinert committed
460
    return Error.NO_DEFAULT_DIR
461

462

463
def get_ldap_server(config=None, server=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
464 465 466 467 468 469 470
    """
    get the LDAP-Server for the FSR lookup
    The servers are used in this order:
    - the server provided by server
    - the server provided by config['SERVER']['NAME']
    - `rincewind.fs.physik.uni-kl.de`
    """
471 472
    if server:
        return server
473
    if config:
474
        return config['SERVER']['NAME']
Lorenz Steinert's avatar
Lorenz Steinert committed
475
    logging.warning('No LDAP Server supplied. Trying to use "rincewind.fs.physik.uni-kl.de".')  # noqa: E501
476
    return "rincewind.fs.physik.uni-kl.de"
477

478

479
def get_ldap_base(config=None, base=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
480 481 482 483 484 485 486
    """
    get the search base for the LDAP-Server
    The bases are used in this order:
    - the base provided by base
    - the base provided by config['SERVER']['BASE']
    - `dc=fs,dc=physik,dc=uni-kl,dc=de`
    """
487 488
    if base:
        return base
489
    if config:
490
        return config['SERVER']['BASE']
Lorenz Steinert's avatar
Lorenz Steinert committed
491
    logging.warning('No LDAP search base supplied. '
Lorenz Steinert's avatar
Lorenz Steinert committed
492
                    'Trying to use "dc=fs,dc=physik,dc=uni-kl,dc=de".')
493
    return "dc=fs,dc=physik,dc=uni-kl,dc=de"
494

495

496
def get_server_timeout(config=None, test=None):
Lorenz Steinert's avatar
Lorenz Steinert committed
497 498 499 500 501 502 503 504
    """
    get the timeout for the connection to the LDAP-Server
    The timeouts are used in this order:
    - if test is true:
      - `1`
    - config['SERVER']['connect_timeout']
    - `10`
    """
505 506
    if test:
        return 1
507
    if config:
508
        return config['SERVER'].getint('connect_timeout')
509
    return 10