3 # OpenChange provisioning
4 # Copyright (C) Jelmer Vernooij <jelmer@openchange.org> 2008-2009
5 # Copyright (C) Julien Kerihuel <j.kerihuel@openchange.org> 2009
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.
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.
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/>.
21 from base64 import b64encode
23 from openchange import mailbox
24 from samba import Ldb, dsdb
25 from samba.samdb import SamDB
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
32 __docformat__ = 'restructuredText'
34 DEFAULTSITE = "Default-First-Site-Name"
35 FIRST_ORGANIZATION = "First Organization"
36 FIRST_ORGANIZATION_UNIT = "First Administrative Group"
38 # This is a hack. Kind-of cute, but still a hack
41 caller = inspect.getouterframes(inspect.currentframe())[1][3]
42 raise NotImplementedError(caller + ' must be implemented in subclass')
44 # Define an abstraction for progress reporting from the provisioning
45 class AbstractProgressReporter(object):
50 def reportNextStep(self, stepName):
51 self.currentStep = self.currentStep + 1
52 self.doReporting(stepName)
54 def doReporting(self, stepName):
57 # A concrete example of a progress reporter - just provides text output for
59 class TextProgressReporter(AbstractProgressReporter):
60 def doReporting(self, stepName):
61 print "[+] Step %d: %s" % (self.currentStep, stepName)
63 class ProvisionNames(object):
71 self.netbiosname = None
76 self.firstorgdn = None
77 # OpenChange dispatcher database specific
78 self.ocfirstorgdn = None
79 self.ocserverdn = None
81 def guess_names_from_smbconf(lp, firstorg=None, firstou=None):
82 """Guess configuration settings to use from smb.conf.
84 :param lp: Loadparm context.
85 :param firstorg: First Organization
86 :param firstou: First Organization Unit
89 netbiosname = lp.get("netbios name")
90 hostname = netbiosname.lower()
92 dnsdomain = lp.get("realm")
93 dnsdomain = dnsdomain.lower()
95 serverrole = lp.get("server role")
96 if serverrole == "domain controller":
97 domain = lp.get("workgroup")
98 domaindn = "DC=" + dnsdomain.replace(".", ",DC=")
101 domaindn = "CN=" + netbiosname
104 configdn = "CN=Configuration," + rootdn
105 schemadn = "CN=Schema," + configdn
106 sitename = DEFAULTSITE
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
120 firstorg = FIRST_ORGANIZATION
123 firstou = FIRST_ORGANIZATION_UNIT
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)
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)
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
148 session_info = system_session()
150 db = SamDB(url=lp.samdb_url(), session_info=session_info,
151 credentials=creds, lp=lp)
153 db.transaction_start()
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
169 db.transaction_cancel()
172 db.transaction_commit()
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
184 session_info = system_session()
186 db = SamDB(url=lp.samdb_url(), session_info=session_info,
187 credentials=creds, lp=lp)
189 db.transaction_start()
192 reporter.reportNextStep(msg)
193 setup_modify_ldif(db, setup_path(ldif), {
194 "SCHEMADN": names.schemadn,
195 "CONFIGDN": names.configdn
198 db.transaction_cancel()
201 db.transaction_commit()
204 def install_schemas(setup_path, names, lp, creds, reporter):
205 """Install the OpenChange-specific schemas in the SAM LDAP database.
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)
213 session_info = system_session()
215 lp.set("dsdb:schema update allowed", "yes")
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)
221 schemadn = str(names.schemadn)
222 current = samdb.search(expression="objectClass=*", base=schemadn,
229 schema_ldif += samdb.write_ldif(ent, ldb.CHANGETYPE_NONE)
231 prefixmap_data = open(setup_path("AD/prefixMap.txt"), 'r').read()
232 prefixmap_data = b64encode(prefixmap_data)
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)
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!"
252 def newuser(lp, creds, username=None):
253 """extend user record with OpenChange settings.
255 :param lp: Loadparm context
256 :param creds: Credentials context
257 :param username: Name of user to extend
260 names = guess_names_from_smbconf(lp, None, None)
262 db = Ldb(url=lp.samdb_url(), session_info=system_session(),
263 credentials=creds, lp=lp)
265 user_dn = "CN=%s,CN=Users,%s" % (username, names.domaindn)
271 auxiliaryClass: msExchBaseClass
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
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
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)
290 res = db.search(base=user_dn, scope=SCOPE_BASE, attrs=["*"])
295 "this should never happen as we just modified the record..."
296 record_keys = map(lambda x: x.lower(), record.keys())
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)
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)
306 print "[+] User %s extended and enabled" % username
309 def accountcontrol(lp, creds, username=None, value=0):
310 """enable/disable an OpenChange user account.
312 :param lp: Loadparm context
313 :param creds: Credentials context
314 :param username: Name of user to disable
315 :param value: the control value
318 names = guess_names_from_smbconf(lp, None, None)
320 db = Ldb(url=os.path.join(lp.get("private dir"), lp.samdb_url()),
321 session_info=system_session(), credentials=creds, lp=lp)
323 user_dn = "CN=%s,CN=Users,%s" % (username, names.domaindn)
327 replace: msExchUserAccountControl
328 msExchUserAccountControl: %d
329 """ % (user_dn, value)
330 db.modify_ldif(extended_user)
332 print "[+] Account %s disabled" % username
334 print "[+] Account %s enabled" % username
337 def provision(setup_path, lp, creds, firstorg=None, firstou=None, reporter=None):
338 """Extend Samba4 with OpenChange data.
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)
347 If a progress reporter is not provided, a text output reporter is provided
349 names = guess_names_from_smbconf(lp, firstorg, firstou)
351 print "NOTE: This operation can take several minutes"
354 reporter = TextProgressReporter()
356 # Install OpenChange-specific schemas
357 install_schemas(setup_path, names, lp, creds, reporter)
360 def openchangedb_provision(lp, firstorg=None, firstou=None, mapistore=None):
361 """Create the OpenChange database.
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)
368 names = guess_names_from_smbconf(lp, firstorg, firstou)
370 print "Setting up openchange db"
371 openchange_ldb = mailbox.OpenChangeDB(openchangedb_url(lp))
372 openchange_ldb.setup()
374 print "Adding root DSE"
375 openchange_ldb.add_rootDSE(names.ocserverdn, names.firstorg, names.firstou)
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)
382 print "[+] Public Folders"
383 print "==================="
384 openchange_ldb.add_public_folders(names)
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):
396 ret = os.path.join(dirname, "../../setup")
397 if os.path.isdir(ret):
399 raise Exception("Unable to find setup directory.")