1# -*- coding: utf-8 -*-
2#
3"""Bareos Client Configuration Listener Module."""
4
5__package__ = ''  # workaround for PEP 366
6import grp
7import os
8import stat
9import string
10from subprocess import Popen, PIPE, STDOUT
11import random
12
13from listener import setuid, unsetuid
14import univention.debug as ud
15
16name = 'bareos'
17description = 'Generate Bareos Configuration for Clients'
18filter = '(objectClass=bareosClientHost)'
19attributes = []
20
21PATH_PREFIX = '/etc/bareos/autogenerated'
22JOBS_PATH = PATH_PREFIX + '/clients'
23INCLUDES_PATH = PATH_PREFIX + '/clients.include'
24BCONSOLE_CMD = ['/usr/bin/bconsole']
25
26JOB_DISABLED = 'Not'
27
28bareos_gid = grp.getgrnam('bareos').gr_gid
29
30
31def getFqdn(entry):
32    if not entry.has_key('cn'):
33        return None
34
35    name = entry['cn'][0]
36    if entry.has_key('associatedDomain'):
37        name = name + '.' + entry['associatedDomain'][0]
38
39    return name
40
41
42def initialize():
43    """Initialize the module once on first start or after clean."""
44    ud.debug(ud.LISTENER, ud.INFO, 'BAREOS: Initialize')
45
46
47def handler(dn, new, old):
48    """Handle changes to 'dn'."""
49    setuid(0)
50    try:
51        # if configRegistry['server/role'] != 'domaincontroller_master':
52        #       return
53
54        # ud.debug(ud.LISTENER, ud.INFO, 'BAREOS: handler '+dn+' '+str(bareos_gid))
55
56        if new and not old:
57            # changeType: add
58            name = getFqdn(new)
59            processClient(name, new)
60
61        elif old and not new:
62            # changeType: delete
63            try:
64                name = getFqdn(old)
65                processClient(name, old, delete=True)
66            except:
67                pass
68        else:
69            # changeType: modify
70            name = getFqdn(new)
71            processClient(name, new)
72    finally:
73        unsetuid()
74
75
76def clean():
77    """Handle request to clean-up the module."""
78    return
79
80
81def postrun():
82    """Transition from prepared-state to not-prepared."""
83    return
84
85
86def processClient(client_name, entry, delete=False):
87    if client_name is None:
88        return
89
90    client_type = 'generic'
91    if 'univentionWindows' in entry['objectClass']:
92        client_type = 'windows'
93
94    if delete == True:
95        removeClient(client_name, client_type)
96        return
97
98    if entry.has_key('bareosEnableJob'):
99        if entry['bareosEnableJob'][0] == JOB_DISABLED:
100            removeClient(client_name, client_type)
101            return
102
103    addClient(client_name, client_type)
104
105
106def addClient(client_name, client_type):
107    createClientJob(client_name, client_type)
108    addClientInclude(client_name)
109    exportBareosFdDirectorResource(client_name, client_type)
110
111
112def removeClient(client_name, client_type):
113    if client_name == None:
114        return
115    disableClientJob(client_name, client_type)
116    addClientInclude(client_name)
117
118
119def getClientSecret(client_name):
120    path = getClientSecretPath(client_name)
121    password = None
122
123    try:
124        f = open(path, 'r')
125        password = f.read().strip()
126    except:
127        password = createClientSecret(client_name)
128
129    return password
130
131
132def exportBareosFdDirectorResource(client_name, client_type):
133    # send commands via pipe to bconsole
134    process = Popen(BCONSOLE_CMD, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
135    # additional reload required, to gurantee that client is known to director
136    out = process.communicate(
137        b'reload\nconfigure export client="{client_name}-fd"\n'.format(
138            client_name=client_name))[0]
139    ud.debug(ud.LISTENER, ud.INFO, "bareos export output:\n" + str(out))
140
141
142def createClientSecret(client_name):
143    path = getClientSecretPath(client_name)
144
145    char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase
146    password = ''.join(random.sample(char_set * 40, 40))
147    with os.fdopen(
148            os.open(path, os.O_CREAT | os.O_WRONLY,
149                    stat.S_IRUSR | stat.S_IWUSR), 'w') as f:
150        f.write(password)
151    os.chown(path, -1, 0)
152
153    return password
154
155
156def removeClientJob(client_name):
157    path = JOBS_PATH + '/' + client_name + '.include'
158    os.remove(path)
159
160
161def createClientJob(client_name, client_type, enable='Yes'):
162    password = getClientSecret(client_name)
163    path = JOBS_PATH + '/' + client_name + '.include'
164    templatefile = JOBS_PATH + '/' + client_type + '.template'
165    with open(templatefile, 'r') as f:
166        content = f.read()
167
168    t = string.Template(content)
169    with os.fdopen(
170            os.open(path, os.O_CREAT | os.O_WRONLY,
171                    stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP), 'w') as f:
172        f.write(
173            t.substitute(
174                enable=enable, password=password, client_name=client_name))
175    os.chown(path, -1, bareos_gid)
176
177
178def disableClientJob(client_name, client_type):
179    createClientJob(client_name, client_type, 'No')
180
181
182def getClientIncludePath(client_name):
183    return '@' + JOBS_PATH + '/' + client_name + '.include'
184
185
186def getClientSecretPath(client_name):
187    return JOBS_PATH + '/' + client_name + '.secret'
188
189
190def addClientInclude(client_name):
191    # is the client already in the include list?
192    if isClientIncluded(client_name):
193        # update the timestamp on the file
194        # to let the cron script know the configuration
195        # has changed
196        os.utime(INCLUDES_PATH, None)
197        return
198
199    # if not, add it at the end of the file
200    with open(INCLUDES_PATH, 'a') as f:
201        f.write(getClientIncludePath(client_name))
202        f.write('\n')
203
204
205def isClientIncluded(client_name):
206    want = getClientIncludePath(client_name)
207    with open(INCLUDES_PATH, 'r') as f:
208        for l in f.readlines():
209            if want in l:
210                return True
211    return False
212