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
24 from openchange import mailbox
26 from samba.samdb import SamDB
27 from samba.auth import system_session
28 from samba.provision import setup_add_ldif, setup_modify_ldif
29 from openchange.urlutils import openchangedb_url, openchangedb_mapistore_url, openchangedb_mapistore_dir, openchangedb_suffix_for_mapistore_url
31 __docformat__ = 'restructuredText'
33 DEFAULTSITE = "Default-First-Site-Name"
34 FIRST_ORGANIZATION = "First Organization"
35 FIRST_ORGANIZATION_UNIT = "First Organization Unit"
37 # This is a hack. Kind-of cute, but still a hack
40 caller = inspect.getouterframes(inspect.currentframe())[1][3]
41 raise NotImplementedError(caller + ' must be implemented in subclass')
43 # Define an abstraction for progress reporting from the provisioning
44 class AbstractProgressReporter(object):
49 def reportNextStep(self, stepName):
50 self.currentStep = self.currentStep + 1
51 self.doReporting(stepName)
53 def doReporting(self, stepName):
56 # A concrete example of a progress reporter - just provides text output for
58 class TextProgressReporter(AbstractProgressReporter):
59 def doReporting(self, stepName):
60 print "[+] Step %d: %s" % (self.currentStep, stepName)
62 class ProvisionNames(object):
70 self.netbiosname = None
75 self.firstorgdn = None
76 # OpenChange dispatcher database specific
77 self.ocfirstorgdn = None
78 self.ocserverdn = None
80 def guess_names_from_smbconf(lp, firstorg=None, firstou=None):
81 """Guess configuration settings to use from smb.conf.
83 :param lp: Loadparm context.
84 :param firstorg: First Organization
85 :param firstou: First Organization Unit
88 netbiosname = lp.get("netbios name")
89 hostname = netbiosname.lower()
91 dnsdomain = lp.get("realm")
92 dnsdomain = dnsdomain.lower()
94 serverrole = lp.get("server role")
95 if serverrole == "domain controller":
96 domain = lp.get("workgroup")
97 domaindn = "DC=" + dnsdomain.replace(".", ",DC=")
100 domaindn = "CN=" + netbiosname
103 configdn = "CN=Configuration," + rootdn
104 schemadn = "CN=Schema," + configdn
105 sitename = DEFAULTSITE
107 names = ProvisionNames()
108 names.rootdn = rootdn
109 names.domaindn = domaindn
110 names.configdn = configdn
111 names.schemadn = schemadn
112 names.dnsdomain = dnsdomain
113 names.domain = domain
114 names.netbiosname = netbiosname
115 names.hostname = hostname
116 names.sitename = sitename
119 firstorg = FIRST_ORGANIZATION
122 firstou = FIRST_ORGANIZATION_UNIT
124 names.firstorg = firstorg
125 names.firstou = firstou
126 names.firstorgdn = "CN=%s,CN=Microsoft Exchange,CN=Services,%s" % (firstorg, configdn)
127 names.serverdn = "CN=%s,CN=Servers,CN=%s,CN=Sites,%s" % (netbiosname, sitename, configdn)
129 # OpenChange dispatcher DB names
130 names.ocserverdn = "CN=%s,%s" % (names.netbiosname, names.domaindn)
131 names.ocfirstorg = firstorg
132 names.ocfirstorgdn = "CN=%s,CN=%s,%s" % (firstou, names.ocfirstorg, names.ocserverdn)
136 def provision_schema(setup_path, names, lp, creds, reporter, ldif, msg):
137 """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.get("sam database"), 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
177 :param setup_path: Path to the setup directory.
178 :param names: provision names object.
179 :param lp: Loadparm context
180 :param creds: Credentials Context
181 :param reporter: A progress reporter instance (subclass of AbstractProgressReporter)
182 :param ldif: path to the LDIF file
183 :param msg: reporter message
186 session_info = system_session()
188 db = SamDB(url=lp.get("sam database"), session_info=session_info,
189 credentials=creds, lp=lp)
191 db.transaction_start()
194 reporter.reportNextStep(msg)
195 setup_modify_ldif(db, setup_path(ldif), {
196 "SCHEMADN": names.schemadn,
197 "CONFIGDN": names.configdn
200 db.transaction_cancel()
203 db.transaction_commit()
205 def install_schemas(setup_path, names, lp, creds, reporter):
206 """Install the OpenChange-specific schemas in the SAM LDAP database.
208 :param setup_path: Path to the setup directory.
209 :param names: provision names object.
210 :param lp: Loadparm context
211 :param creds: Credentials Context
212 :param reporter: A progress reporter instance (subclass of AbstractProgressReporter)
214 session_info = system_session()
216 # Step 1. Extending the prefixmap attribute of the schema DN record
217 db = SamDB(url=lp.get("sam database"), session_info=session_info,
218 credentials=creds, lp=lp)
220 prefixmap = open(setup_path("AD/prefixMap.txt"), 'r').read()
222 db.transaction_start()
225 reporter.reportNextStep("Register Exchange OIDs")
226 setup_modify_ldif(db,
227 setup_path("AD/provision_schema_basedn_modify.ldif"), {
228 "SCHEMADN": names.schemadn,
229 "NETBIOSNAME": names.netbiosname,
230 "DEFAULTSITE": names.sitename,
231 "CONFIGDN": names.configdn,
232 "SERVERDN": names.serverdn,
233 "PREFIXMAP_B64": b64encode(prefixmap)
236 db.transaction_cancel()
239 db.transaction_commit()
241 provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_attributes.ldif", "Add Exchange attributes to Samba schema")
242 provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_auxiliary_class.ldif", "Add Exchange auxiliary classes to Samba schema")
243 provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_objectCategory.ldif", "Add Exchange objectCategory to Samba schema")
244 provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_container.ldif", "Add Exchange containers to Samba schema")
245 provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_subcontainer.ldif", "Add Exchange *sub* containers to Samba schema")
246 provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_sub_CfgProtocol.ldif", "Add Exchange CfgProtocol subcontainers to Samba schema")
247 provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_sub_mailGateway.ldif", "Add Exchange mailGateway subcontainers to Samba schema")
248 provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema.ldif", "Add Exchange classes to Samba schema")
249 modify_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_possSuperior.ldif", "Add possSuperior attributes to Exchange classes")
250 modify_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_schema_modify.ldif", "Extend existing Samba classes and attributes")
251 provision_schema(setup_path, names, lp, creds, reporter, "AD/oc_provision_configuration.ldif", "Exchange Samba with Exchange configuration objects")
252 print "[SUCCESS] Done!"
254 def newmailbox(lp, username, firstorg, firstou, backend):
255 names = guess_names_from_smbconf(lp, firstorg, firstou)
257 db = mailbox.OpenChangeDB(openchangedb_url(lp))
259 # Step 1. Retrieve current FID index
260 GlobalCount = db.get_message_GlobalCount(names.netbiosname)
261 ReplicaID = db.get_message_ReplicaID(names.netbiosname)
263 print "[+] Mailbox for '%s'" % (username)
264 print "==================" + "=" * len(username)
265 print "* GlobalCount (0x%x) and ReplicaID (0x%x)" % (GlobalCount, ReplicaID)
267 # Step 2. Check if the user already exists
268 assert not db.user_exists(names.netbiosname, username)
270 # Step 3. Create a default mapistore content repository for this user
271 db.add_storage_dir(mapistoreURL=openchangedb_mapistore_dir(lp), username=username)
272 print "* Mapistore content repository created: %s" % os.path.join(openchangedb_mapistore_dir(lp), username)
274 # Step 4. Create the user object
275 retdn = db.add_mailbox_user(names.ocfirstorgdn, username=username)
276 print "* User object created: %s" % (retdn)
278 # Step 5. Create system mailbox folders for this user
279 print "* Adding System Folders"
282 "Deferred Actions": ({}, 2),
283 "Spooler Queue": ({}, 3),
284 "To-Do Search": ({}, 4),
288 "Sent Items": ({}, 8),
289 "Deleted Items": ({}, 9),
291 "Common Views": ({}, 10),
292 "Schedule": ({}, 11),
295 "Shortcuts": ({}, 14),
296 "Reminders": ({}, 15),
300 def add_folder(parent_fid, path, children, SystemIdx):
303 GlobalCount = db.get_message_GlobalCount(names.netbiosname)
304 ReplicaID = db.get_message_ReplicaID(names.netbiosname)
305 url = openchangedb_mapistore_url(lp, backend)
307 fid = db.add_mailbox_root_folder(names.ocfirstorgdn,
308 username=username, foldername=name,
309 parentfolder=parent_fid, GlobalCount=GlobalCount,
310 ReplicaID=ReplicaID, SystemIdx=SystemIdx,
312 mapistoreSuffix=openchangedb_suffix_for_mapistore_url(url))
315 db.set_message_GlobalCount(names.netbiosname, GlobalCount=GlobalCount)
319 print "\t* %-40s: %s" % (name, fid)
320 for name, grandchildren in children.iteritems():
321 add_folder(fid, path + (name,), grandchildren[0], grandchildren[1])
323 add_folder(0, ("Mailbox Root",), system_folders[0], system_folders[1])
325 # Step 6. Add special folders
326 print "* Adding Special Folders:"
328 (("Mailbox Root", "IPM Subtree"), "Calendar", "IPF.Appointment", "PidTagIpmAppointmentEntryId"),
329 (("Mailbox Root", "IPM Subtree"), "Contacts", "IPF.Contact", "PidTagIpmContactEntryId"),
330 (("Mailbox Root", "IPM Subtree"), "Journal", "IPF.Journal", "PidTagIpmJournalEntryId"),
331 (("Mailbox Root", "IPM Subtree"), "Notes", "IPF.StickyNote", "PidTagIpmNoteEntryId"),
332 (("Mailbox Root", "IPM Subtree"), "Tasks", "IPF.Task", "PidTagIpmTaskEntryId"),
333 (("Mailbox Root", "IPM Subtree"), "Drafts", "IPF.Note", "PidTagIpmDraftsEntryId")
336 fid_inbox = fids[("Mailbox Root", "IPM Subtree", "Inbox")]
337 fid_reminders = fids[("Mailbox Root", "Reminders")]
338 fid_mailbox = fids[("Mailbox Root",)]
339 for path, foldername, containerclass, pidtag in special_folders:
340 GlobalCount = db.get_message_GlobalCount(names.netbiosname)
341 ReplicaID = db.get_message_ReplicaID(names.netbiosname)
342 url = openchangedb_mapistore_url(lp, backend)
343 fid = db.add_mailbox_special_folder(username, fids[path], fid_inbox, foldername,
344 containerclass, GlobalCount, ReplicaID,
345 url, openchangedb_suffix_for_mapistore_url(url))
346 db.add_folder_property(fid_inbox, pidtag, fid)
347 db.add_folder_property(fid_mailbox, pidtag, fid)
349 db.set_message_GlobalCount(names.netbiosname, GlobalCount=GlobalCount)
350 print "\t* %-40s: %s (%s)" % (foldername, fid, containerclass)
352 # Step 7. Set default receive folders
353 print "* Adding default Receive Folders:"
355 (("Mailbox Root", "IPM Subtree", "Inbox"), "All"),
356 (("Mailbox Root", "IPM Subtree", "Inbox"), "IPM"),
357 (("Mailbox Root", "IPM Subtree", "Inbox"), "Report.IPM"),
358 (("Mailbox Root", "IPM Subtree", "Inbox"), "IPM.Note"),
359 (("Mailbox Root", "IPM Subtree",), "IPC")
362 for path, messageclass in receive_folders:
363 print "\t* %-40s Message Class added to %s" % (messageclass, fids[path])
364 db.set_receive_folder(username, names.ocfirstorgdn, fids[path],
367 # Step 8. Set additional properties on Inbox
368 print "* Adding additional default properties to Inbox"
369 db.add_folder_property(fid_inbox, "PidTagContentCount", "0")
370 db.add_folder_property(fid_inbox, "PidTagContentUnreadCount", "0")
371 db.add_folder_property(fid_inbox, "PidTagSubFolders", "FALSE")
373 print "* Adding additional default properties to Reminders"
374 db.add_folder_property(fid_reminders, "PidTagContainerClass", "Outlook.Reminder");
375 db.add_folder_property(fid_inbox, "PidTagRemindersOnlineEntryId", fid_reminders);
376 db.add_folder_property(fid_mailbox, "PidTagRemindersOnlineEntryId", fid_reminders);
378 GlobalCount = db.get_message_GlobalCount(names.netbiosname)
379 print "* GlobalCount (0x%x)" % GlobalCount
382 def newuser(lp, creds, username=None):
383 """extend user record with OpenChange settings.
385 :param lp: Loadparm context
386 :param creds: Credentials context
387 :param username: Name of user to extend
390 names = guess_names_from_smbconf(lp, None, None)
392 db = Ldb(url=os.path.join(lp.get("private dir"), lp.get("sam database")),
393 session_info=system_session(), credentials=creds, lp=lp)
395 user_dn = "CN=%s,CN=Users,%s" % (username, names.domaindn)
403 auxiliaryClass: msExchBaseClass
407 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
408 add: legacyExchangeDN
409 legacyExchangeDN: /o=%s/ou=First Administrative Group/cn=Recipients/cn=%s
411 proxyAddresses: smtp:postmaster@%s
412 proxyAddresses: X400:c=US;a= ;p=First Organizati;o=Exchange;s=%s
413 proxyAddresses: SMTP:%s@%s
414 replace: msExchUserAccountControl
415 msExchUserAccountControl: 0
416 """ % (user_dn, username, username, names.netbiosname, names.netbiosname, names.firstorg, names.domaindn, names.firstorg, username, names.dnsdomain, username, username, names.dnsdomain)
417 db.modify_ldif(extended_user)
419 print "[+] User %s extended and enabled" % username
422 def accountcontrol(lp, creds, username=None, value=0):
423 """enable/disable an OpenChange user account.
425 :param lp: Loadparm context
426 :param creds: Credentials context
427 :param username: Name of user to disable
428 :param value: the control value
431 names = guess_names_from_smbconf(lp, None, None)
433 db = Ldb(url=os.path.join(lp.get("private dir"), lp.get("sam database")),
434 session_info=system_session(), credentials=creds, lp=lp)
436 user_dn = "CN=%s,CN=Users,%s" % (username, names.domaindn)
440 replace: msExchUserAccountControl
441 msExchUserAccountControl: %d
442 """ % (user_dn, value)
443 db.modify_ldif(extended_user)
445 print "[+] Account %s disabled" % username
447 print "[+] Account %s enabled" % username
450 def provision(setup_path, lp, creds, firstorg=None, firstou=None, reporter=None):
451 """Extend Samba4 with OpenChange data.
453 :param setup_path: Path to the setup directory
454 :param lp: Loadparm context
455 :param creds: Credentials context
456 :param firstorg: First Organization
457 :param firstou: First Organization Unit
458 :param reporter: A progress reporter instance (subclass of AbstractProgressReporter)
460 If a progress reporter is not provided, a text output reporter is provided
462 names = guess_names_from_smbconf(lp, firstorg, firstou)
464 print "NOTE: This operation can take several minutes"
467 reporter = TextProgressReporter()
469 # Install OpenChange-specific schemas
470 install_schemas(setup_path, names, lp, creds, reporter)
473 def openchangedb_provision(lp, firstorg=None, firstou=None, mapistore=None):
474 """Create the OpenChange database.
476 :param lp: Loadparm context
477 :param firstorg: First Organization
478 :param firstou: First Organization Unit
479 :param mapistore: The public folder store type (fsocpf, sqlite, etc)
481 names = guess_names_from_smbconf(lp, firstorg, firstou)
483 print "Setting up openchange db"
484 openchange_ldb = mailbox.OpenChangeDB(openchangedb_url(lp))
485 openchange_ldb.setup()
487 openchange_ldb.add_rootDSE(names.ocserverdn, names.firstorg, names.firstou)
489 # Add a server object
490 # It is responsible for holding the GlobalCount identifier (48 bytes)
491 # and the Replica identifier
492 openchange_ldb.add_server(names.ocserverdn, names.netbiosname,
493 names.firstorg, names.firstou)
495 mapistoreURL = os.path.join( openchangedb_mapistore_url(lp, mapistore), "publicfolders")
496 print "[+] Public Folders"
497 print "==================="
498 openchange_ldb.add_public_folders(names, mapistoreURL)
500 def find_setup_dir():
501 """Find the setup directory used by provision."""
502 dirname = os.path.dirname(__file__)
503 if "/site-packages/" in dirname:
504 prefix = dirname[:dirname.index("/site-packages/")]
505 for suffix in ["share/openchange/setup", "share/setup", "share/samba/setup", "setup"]:
506 ret = os.path.join(prefix, suffix)
507 if os.path.isdir(ret):
510 ret = os.path.join(dirname, "../../setup")
511 if os.path.isdir(ret):
513 raise Exception("Unable to find setup directory.")