4cb4df13dbabf61ddf69e2b13c5cc5f36376d74b
[jelmer/openchange.git] / python / openchange / provision.py
1 #!/usr/bin/python
2
3 # OpenChange provisioning
4 # Copyright (C) Jelmer Vernooij <jelmer@openchange.org> 2008-2009
5 # Copyright (C) Julien Kerihuel <j.kerihuel@openchange.org> 2009
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #   
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #   
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20
21 from base64 import b64encode
22 import os
23 from openchange import mailbox
24 from samba import Ldb, dsdb
25 from samba.samdb import SamDB
26 import ldb
27 from ldb import SCOPE_SUBTREE
28 from samba.auth import system_session
29 from samba.provision import (setup_add_ldif, setup_modify_ldif)
30 from openchange.urlutils import openchangedb_url
31
32 __docformat__ = 'restructuredText'
33
34 DEFAULTSITE = "Default-First-Site-Name"
35 FIRST_ORGANIZATION = "First Organization"
36 FIRST_ORGANIZATION_UNIT = "First Administrative Group"
37
38 # This is a hack. Kind-of cute, but still a hack
39 def abstract():
40     import inspect
41     caller = inspect.getouterframes(inspect.currentframe())[1][3]
42     raise NotImplementedError(caller + ' must be implemented in subclass')
43
44 # Define an abstraction for progress reporting from the provisioning
45 class AbstractProgressReporter(object):
46
47     def __init__(self):
48         self.currentStep = 0
49
50     def reportNextStep(self, stepName):
51         self.currentStep = self.currentStep + 1
52         self.doReporting(stepName)
53
54     def doReporting(self, stepName):
55         abstract()
56
57 # A concrete example of a progress reporter - just provides text output for
58 # each new step.
59 class TextProgressReporter(AbstractProgressReporter):
60     def doReporting(self, stepName):
61         print "[+] Step %d: %s" % (self.currentStep, stepName)
62
63 class ProvisionNames(object):
64
65     def __init__(self):
66         self.rootdn = None
67         self.domaindn = None
68         self.configdn = None
69         self.schemadn = None
70         self.dnsdomain = None
71         self.netbiosname = None
72         self.domain = None
73         self.hostname = None
74         self.firstorg = None
75         self.firstou = None
76         self.firstorgdn = None
77         # OpenChange dispatcher database specific
78         self.ocfirstorgdn = None
79         self.ocserverdn = None
80
81 def guess_names_from_smbconf(lp, firstorg=None, firstou=None):
82     """Guess configuration settings to use from smb.conf.
83     
84     :param lp: Loadparm context.
85     :param firstorg: First Organization
86     :param firstou: First Organization Unit
87     """
88
89     netbiosname = lp.get("netbios name")
90     hostname = netbiosname.lower()
91
92     dnsdomain = lp.get("realm")
93     dnsdomain = dnsdomain.lower()
94
95     serverrole = lp.get("server role")
96     if serverrole == "domain controller":
97         domain = lp.get("workgroup")
98         domaindn = "DC=" + dnsdomain.replace(".", ",DC=")
99     else:
100         domain = netbiosname
101         domaindn = "CN=" + netbiosname
102
103     rootdn = domaindn
104     configdn = "CN=Configuration," + rootdn
105     schemadn = "CN=Schema," + configdn
106     sitename = DEFAULTSITE
107
108     names = ProvisionNames()
109     names.rootdn = rootdn
110     names.domaindn = domaindn
111     names.configdn = configdn
112     names.schemadn = schemadn
113     names.dnsdomain = dnsdomain
114     names.domain = domain
115     names.netbiosname = netbiosname
116     names.hostname = hostname
117     names.sitename = sitename
118
119     if firstorg is None:
120         firstorg = FIRST_ORGANIZATION
121
122     if firstou is None:
123         firstou = FIRST_ORGANIZATION_UNIT
124
125     names.firstorg = firstorg
126     names.firstou = firstou
127     names.firstorgdn = "CN=%s,CN=Microsoft Exchange,CN=Services,%s" % (firstorg, configdn)
128     names.serverdn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % (netbiosname, sitename, configdn)
129
130     # OpenChange dispatcher DB names
131     names.ocserverdn = "CN=%s,%s" % (names.netbiosname, names.domaindn)
132     names.ocfirstorg = firstorg
133     names.ocfirstorgdn = "CN=%s,CN=%s,%s" % (firstou, names.ocfirstorg, names.ocserverdn)
134
135     return names
136
137 def provision_schema(setup_path, names, lp, creds, reporter, ldif, msg):
138     """Provision schema using LDIF specified file
139     :param setup_path: Path to the setup directory.
140     :param names: provision names object.
141     :param lp: Loadparm context
142     :param creds: Credentials Context
143     :param reporter: A progress reporter instance (subclass of AbstractProgressReporter)
144     :param ldif: path to the LDIF file
145     :param msg: reporter message
146     """
147
148     session_info = system_session()
149
150     db = SamDB(url=lp.samdb_url(), session_info=session_info,
151                credentials=creds, lp=lp)
152
153     db.transaction_start()
154
155     try:
156         reporter.reportNextStep(msg)
157         setup_add_ldif(db, setup_path(ldif), {
158                 "FIRSTORG": names.firstorg,
159                 "FIRSTORGDN": names.firstorgdn,
160                 "CONFIGDN": names.configdn,
161                 "SCHEMADN": names.schemadn,
162                 "DOMAINDN": names.domaindn,
163                 "DOMAIN": names.domain,
164                 "DNSDOMAIN": names.dnsdomain,
165                 "NETBIOSNAME": names.netbiosname,
166                 "HOSTNAME": names.hostname
167                 })
168     except:
169         db.transaction_cancel()
170         raise
171
172     db.transaction_commit()
173
174 def modify_schema(setup_path, names, lp, creds, reporter, ldif, msg):
175     """Modify schema using LDIF specified file                                                                                                                                                                          :param setup_path: Path to the setup directory.
176     :param names: provision names object.
177     :param lp: Loadparm context
178     :param creds: Credentials Context
179     :param reporter: A progress reporter instance (subclass of AbstractProgressReporter)
180     :param ldif: path to the LDIF file
181     :param msg: reporter message
182     """
183
184     session_info = system_session()
185
186     db = SamDB(url=lp.samdb_url(), session_info=session_info,
187                credentials=creds, lp=lp)
188
189     db.transaction_start()
190
191     try:
192         reporter.reportNextStep(msg)
193         setup_modify_ldif(db, setup_path(ldif), {
194                 "SCHEMADN": names.schemadn,
195                 "CONFIGDN": names.configdn
196                 })
197     except:
198         db.transaction_cancel()
199         raise
200
201     db.transaction_commit()
202
203
204 def install_schemas(setup_path, names, lp, creds, reporter):
205     """Install the OpenChange-specific schemas in the SAM LDAP database. 
206     
207     :param setup_path: Path to the setup directory.
208     :param names: provision names object.
209     :param lp: Loadparm context
210     :param creds: Credentials Context
211     :param reporter: A progress reporter instance (subclass of AbstractProgressReporter)
212     """
213     session_info = system_session()
214
215     lp.set("dsdb:schema update allowed", "yes")
216
217     # Step 1. Extending the prefixmap attribute of the schema DN record
218     samdb = SamDB(url=lp.samdb_url(), session_info=session_info,
219                   credentials=creds, lp=lp)
220
221     schemadn = str(names.schemadn)
222     current = samdb.search(expression="objectClass=*", base=schemadn, 
223                            scope=SCOPE_SUBTREE)
224     
225
226     schema_ldif = ""
227     prefixmap_data = ""
228     for ent in current:
229         schema_ldif += samdb.write_ldif(ent, ldb.CHANGETYPE_NONE)
230
231     prefixmap_data = open(setup_path("AD/prefixMap.txt"), 'r').read()
232     prefixmap_data = b64encode(prefixmap_data)
233
234     # We don't actually add this ldif, just parse it
235     prefixmap_ldif = "dn: %s\nprefixMap:: %s\n\n" % (schemadn, prefixmap_data)
236     reporter.reportNextStep("Register Exchange OIDs")
237     dsdb._dsdb_set_schema_from_ldif(samdb, prefixmap_ldif, schema_ldif, schemadn)
238
239     provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_attributes.ldif", "Add Exchange attributes to Samba schema")
240     provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_auxiliary_class.ldif", "Add Exchange auxiliary classes to Samba schema")
241     provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_objectCategory.ldif", "Add Exchange objectCategory to Samba schema")
242     provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_container.ldif", "Add Exchange containers to Samba schema")
243     provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_subcontainer.ldif", "Add Exchange *sub* containers to Samba schema")
244     provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_sub_CfgProtocol.ldif", "Add Exchange CfgProtocol subcontainers to Samba schema")
245     provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_sub_mailGateway.ldif", "Add Exchange mailGateway subcontainers to Samba schema")
246     provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema.ldif", "Add Exchange classes to Samba schema")
247     modify_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_possSuperior.ldif", "Add possSuperior attributes to Exchange classes")
248     modify_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_modify.ldif", "Extend existing Samba classes and attributes")
249     provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_configuration.ldif", "Exchange Samba with Exchange configuration objects")
250     print "[SUCCESS] Done!"
251
252 def newuser(lp, creds, username=None):
253     """extend user record with OpenChange settings.
254     
255     :param lp: Loadparm context
256     :param creds: Credentials context
257     :param username: Name of user to extend
258     """
259
260     names = guess_names_from_smbconf(lp, None, None)
261
262     db = Ldb(url=lp.samdb_url(), session_info=system_session(), 
263              credentials=creds, lp=lp)
264
265     user_dn = "CN=%s,CN=Users,%s" % (username, names.domaindn)
266
267     extended_user = """
268 dn: %s
269 changetype: modify
270 add: auxiliaryClass
271 auxiliaryClass: msExchBaseClass
272 add: mailNickName
273 mailNickname: %s
274 add: homeMDB
275 homeMDB: CN=Mailbox Store (%s),CN=First Storage Group,CN=InformationStore,CN=%s,CN=Servers,CN=First Administrative Group,CN=Administrative Groups,CN=%s,CN=Microsoft Exchange,CN=Services,CN=Configuration,%s
276 add: homeMTA
277 homeMTA: CN=Mailbox Store (%s),CN=First Storage Group,CN=InformationStore,CN=%s,CN=Servers,CN=First Administrative Group,CN=Administrative Groups,CN=%s,CN=Microsoft Exchange,CN=Services,CN=Configuration,%s
278 add: legacyExchangeDN
279 legacyExchangeDN: /o=%s/ou=First Administrative Group/cn=Recipients/cn=%s
280 add: proxyAddresses
281 proxyAddresses: =EX:/o=%s/ou=First Administrative Group/cn=Recipients/cn=%s
282 proxyAddresses: smtp:postmaster@%s
283 proxyAddresses: X400:c=US;a= ;p=First Organizati;o=Exchange;s=%s
284 proxyAddresses: SMTP:%s@%s
285 replace: msExchUserAccountControl
286 msExchUserAccountControl: 0
287 """ % (user_dn, username, names.netbiosname, names.netbiosname, names.firstorg, names.domaindn, names.netbiosname, names.netbiosname, names.firstorg, names.domaindn, names.firstorg, username, names.firstorg, username, names.dnsdomain, username, username, names.dnsdomain)
288     db.modify_ldif(extended_user)
289
290     res = db.search(base=user_dn, scope=SCOPE_BASE, attrs=["*"])
291     if len(res) == 1:
292         record = res[0]
293     else:
294         raise Exception, \
295             "this should never happen as we just modified the record..."
296     record_keys = map(lambda x: x.lower(), record.keys())
297
298     if "displayname" not in record_keys:
299         extended_user = "dn: %s\nadd: displayName\ndisplayName: %s\n" % (user_dn, username)
300         db.modify_ldif(extended_user)
301
302     if "mail" not in record_keys:
303         extended_user = "dn: %s\nadd: mail\nmail: %s@%s\n" % (user_dn, username, names.dnsdomain)
304         db.modify_ldif(extended_user)
305
306     print "[+] User %s extended and enabled" % username
307
308
309 def accountcontrol(lp, creds, username=None, value=0):
310     """enable/disable an OpenChange user account.
311
312     :param lp: Loadparm context
313     :param creds: Credentials context
314     :param username: Name of user to disable
315     :param value: the control value
316     """
317
318     names = guess_names_from_smbconf(lp, None, None)
319
320     db = Ldb(url=os.path.join(lp.get("private dir"), lp.samdb_url()), 
321              session_info=system_session(), credentials=creds, lp=lp)
322
323     user_dn = "CN=%s,CN=Users,%s" % (username, names.domaindn)
324     extended_user = """
325 dn: %s
326 changetype: modify
327 replace: msExchUserAccountControl
328 msExchUserAccountControl: %d
329 """ % (user_dn, value)
330     db.modify_ldif(extended_user)
331     if value == 2:
332         print "[+] Account %s disabled" % username
333     else:
334         print "[+] Account %s enabled" % username
335
336
337 def provision(setup_path, lp, creds, firstorg=None, firstou=None, reporter=None):
338     """Extend Samba4 with OpenChange data.
339     
340     :param setup_path: Path to the setup directory
341     :param lp: Loadparm context
342     :param creds: Credentials context
343     :param firstorg: First Organization
344     :param firstou: First Organization Unit
345     :param reporter: A progress reporter instance (subclass of AbstractProgressReporter)
346
347     If a progress reporter is not provided, a text output reporter is provided
348     """
349     names = guess_names_from_smbconf(lp, firstorg, firstou)
350
351     print "NOTE: This operation can take several minutes"
352
353     if reporter is None:
354         reporter = TextProgressReporter()
355
356     # Install OpenChange-specific schemas
357     install_schemas(setup_path, names, lp, creds, reporter)
358
359
360 def openchangedb_provision(lp, firstorg=None, firstou=None, mapistore=None):
361     """Create the OpenChange database.
362
363     :param lp: Loadparm context
364     :param firstorg: First Organization
365     :param firstou: First Organization Unit
366     :param mapistore: The public folder store type (fsocpf, sqlite, etc)
367     """
368     names = guess_names_from_smbconf(lp, firstorg, firstou)
369     
370     print "Setting up openchange db"
371     openchange_ldb = mailbox.OpenChangeDB(openchangedb_url(lp))
372     openchange_ldb.setup()
373
374     print "Adding root DSE"
375     openchange_ldb.add_rootDSE(names.ocserverdn, names.firstorg, names.firstou)
376
377     # Add a server object
378     # It is responsible for holding the GlobalCount identifier (48 bytes)
379     # and the Replica identifier
380     openchange_ldb.add_server(names.ocserverdn, names.netbiosname, names.firstorg, names.firstou)
381
382     print "[+] Public Folders"
383     print "==================="
384     openchange_ldb.add_public_folders(names)
385
386 def find_setup_dir():
387     """Find the setup directory used by provision."""
388     dirname = os.path.dirname(__file__)
389     if "/site-packages/" in dirname:
390         prefix = dirname[:dirname.index("/site-packages/")]
391         for suffix in ["share/openchange/setup", "share/setup", "share/samba/setup", "setup"]:
392             ret = os.path.join(prefix, suffix)
393             if os.path.isdir(ret):
394                 return ret
395     # In source tree
396     ret = os.path.join(dirname, "../../setup")
397     if os.path.isdir(ret):
398         return ret
399     raise Exception("Unable to find setup directory.")