server: intialize aux_header buffer to null if the data is missing.
[tridge/openchange.git] / branches / plugfest / 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 import samba
24 from openchange import mailbox
25 from samba import Ldb
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
30
31 __docformat__ = 'restructuredText'
32
33 DEFAULTSITE = "Default-First-Site-Name"
34 FIRST_ORGANIZATION = "First Organization"
35 FIRST_ORGANIZATION_UNIT = "First Organization Unit"
36
37 # This is a hack. Kind-of cute, but still a hack
38 def abstract():
39     import inspect
40     caller = inspect.getouterframes(inspect.currentframe())[1][3]
41     raise NotImplementedError(caller + ' must be implemented in subclass')
42
43 # Define an abstraction for progress reporting from the provisioning
44 class AbstractProgressReporter(object):
45
46     def __init__(self):
47         self.currentStep = 0
48
49     def reportNextStep(self, stepName):
50         self.currentStep = self.currentStep + 1
51         self.doReporting(stepName)
52
53     def doReporting(self, stepName):
54         abstract()
55
56 # A concrete example of a progress reporter - just provides text output for
57 # each new step.
58 class TextProgressReporter(AbstractProgressReporter):
59     def doReporting(self, stepName):
60         print "[+] Step %d: %s" % (self.currentStep, stepName)
61
62 class ProvisionNames(object):
63
64     def __init__(self):
65         self.rootdn = None
66         self.domaindn = None
67         self.configdn = None
68         self.schemadn = None
69         self.dnsdomain = None
70         self.netbiosname = None
71         self.domain = None
72         self.hostname = None
73         self.firstorg = None
74         self.firstou = None
75         self.firstorgdn = None
76         # OpenChange dispatcher database specific
77         self.ocfirstorgdn = None
78         self.ocserverdn = None
79
80 def guess_names_from_smbconf(lp, firstorg=None, firstou=None):
81     """Guess configuration settings to use from smb.conf.
82     
83     :param lp: Loadparm context.
84     :param firstorg: First Organization
85     :param firstou: First Organization Unit
86     """
87
88     netbiosname = lp.get("netbios name")
89     hostname = netbiosname.lower()
90
91     dnsdomain = lp.get("realm")
92     dnsdomain = dnsdomain.lower()
93
94     serverrole = lp.get("server role")
95     if serverrole == "domain controller":
96         domain = lp.get("workgroup")
97         domaindn = "DC=" + dnsdomain.replace(".", ",DC=")
98     else:
99         domain = netbiosname
100         domaindn = "CN=" + netbiosname
101
102     rootdn = domaindn
103     configdn = "CN=Configuration," + rootdn
104     schemadn = "CN=Schema," + configdn
105     sitename = DEFAULTSITE
106
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
117
118     if firstorg is None:
119         firstorg = FIRST_ORGANIZATION
120
121     if firstou is None:
122         firstou = FIRST_ORGANIZATION_UNIT
123
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)
128
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)
133
134     return names
135
136 def provision_schema(setup_path, names, lp, creds, reporter, ldif, msg):
137     """Provision schema using LDIF specified file
138
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.get("sam database"), 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
176
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
184     """
185
186     session_info = system_session()
187
188     db = SamDB(url=lp.get("sam database"), session_info=session_info, 
189                credentials=creds, lp=lp)
190
191     db.transaction_start()
192
193     try:
194         reporter.reportNextStep(msg)
195         setup_modify_ldif(db, setup_path(ldif), { 
196                 "SCHEMADN": names.schemadn,
197                 "CONFIGDN": names.configdn
198                 })
199     except:
200         db.transaction_cancel()
201         raise
202
203     db.transaction_commit()
204
205 def install_schemas(setup_path, names, lp, creds, reporter):
206     """Install the OpenChange-specific schemas in the SAM LDAP database. 
207     
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)
213     """
214     session_info = system_session()
215
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)
219
220     prefixmap = open(setup_path("AD/prefixMap.txt"), 'r').read()
221
222     db.transaction_start()
223
224     try:
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)
234                 })
235     except:
236         db.transaction_cancel()
237         raise
238
239     db.transaction_commit()
240
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!"
253
254 def newmailbox(lp, username, firstorg, firstou, backend):
255     names = guess_names_from_smbconf(lp, firstorg, firstou)
256
257     db = mailbox.OpenChangeDB(openchangedb_url(lp))
258
259     # Step 1. Retrieve current FID index
260     GlobalCount = db.get_message_GlobalCount(names.netbiosname)
261     ReplicaID = db.get_message_ReplicaID(names.netbiosname)
262
263     print "[+] Mailbox for '%s'" % (username)
264     print "==================" + "=" * len(username)
265     print "* GlobalCount (0x%x) and ReplicaID (0x%x)" % (GlobalCount, ReplicaID)
266
267     # Step 2. Check if the user already exists
268     assert not db.user_exists(names.netbiosname, username)
269
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)
273
274     # Step 4. Create the user object
275     retdn = db.add_mailbox_user(names.ocfirstorgdn, username=username)
276     print "* User object created: %s" % (retdn)
277
278     # Step 5. Create system mailbox folders for this user
279     print "* Adding System Folders"
280
281     system_folders = ({
282         "Deferred Actions": ({}, 2),
283         "Spooler Queue": ({}, 3),
284         "To-Do Search": ({}, 4),
285         "IPM Subtree": ({
286             "Inbox": ({}, 6),
287             "Outbox": ({}, 7),
288             "Sent Items": ({}, 8),
289             "Deleted Items": ({}, 9),
290         }, 5),
291         "Common Views": ({}, 10),
292         "Schedule": ({}, 11),
293         "Search": ({}, 12),
294         "Views": ({}, 13),
295         "Shortcuts": ({}, 14),
296         "Reminders": ({}, 15),
297     }, 1)
298
299     fids = {}
300     def add_folder(parent_fid, path, children, SystemIdx):
301         name = path[-1]
302
303         GlobalCount = db.get_message_GlobalCount(names.netbiosname)
304         ReplicaID = db.get_message_ReplicaID(names.netbiosname)
305         url = openchangedb_mapistore_url(lp, backend)
306
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, 
311             mapistoreURL=url,
312             mapistoreSuffix=openchangedb_suffix_for_mapistore_url(url))
313
314         GlobalCount += 1
315         db.set_message_GlobalCount(names.netbiosname, GlobalCount=GlobalCount)
316
317         fids[path] = fid
318
319         print "\t* %-40s: %s" % (name, fid)
320         for name, grandchildren in children.iteritems():
321             add_folder(fid, path + (name,), grandchildren[0], grandchildren[1])
322
323     add_folder(0, ("Mailbox Root",), system_folders[0], system_folders[1])
324
325     # Step 6. Add special folders
326     print "* Adding Special Folders:"
327     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")
334         ]
335
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)
348         GlobalCount += 1
349         db.set_message_GlobalCount(names.netbiosname, GlobalCount=GlobalCount)
350         print "\t* %-40s: %s (%s)" % (foldername, fid, containerclass)
351
352     # Step 7. Set default receive folders
353     print "* Adding default Receive Folders:"
354     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")
360         ]
361     
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], 
365                               messageclass)
366
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")
372
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);
377
378     GlobalCount = db.get_message_GlobalCount(names.netbiosname)
379     print "* GlobalCount (0x%x)" % GlobalCount
380
381
382 def newuser(lp, creds, username=None):
383     """extend user record with OpenChange settings.
384     
385     :param lp: Loadparm context
386     :param creds: Credentials context
387     :param username: Name of user to extend
388     """
389
390     names = guess_names_from_smbconf(lp, None, None)
391
392     db = Ldb(url=os.path.join(lp.get("private dir"), lp.get("sam database")), 
393              session_info=system_session(), credentials=creds, lp=lp)
394
395     user_dn = "CN=%s,CN=Users,%s" % (username, names.domaindn)
396
397     extended_user = """
398 dn: %s
399 changetype: modify
400 add: displayName
401 displayName: %s
402 add: auxiliaryClass
403 auxiliaryClass: msExchBaseClass
404 add: mailNickName
405 mailNickname: %s
406 add: homeMDB
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
410 add: proxyAddresses
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)
418
419     print "[+] User %s extended and enabled" % username
420
421
422 def accountcontrol(lp, creds, username=None, value=0):
423     """enable/disable an OpenChange user account.
424
425     :param lp: Loadparm context
426     :param creds: Credentials context
427     :param username: Name of user to disable
428     :param value: the control value
429     """
430
431     names = guess_names_from_smbconf(lp, None, None)
432
433     db = Ldb(url=os.path.join(lp.get("private dir"), lp.get("sam database")), 
434              session_info=system_session(), credentials=creds, lp=lp)
435
436     user_dn = "CN=%s,CN=Users,%s" % (username, names.domaindn)
437     extended_user = """
438 dn: %s
439 changetype: modify
440 replace: msExchUserAccountControl
441 msExchUserAccountControl: %d
442 """ % (user_dn, value)
443     db.modify_ldif(extended_user)
444     if value == 2:
445         print "[+] Account %s disabled" % username
446     else:
447         print "[+] Account %s enabled" % username
448
449
450 def provision(setup_path, lp, creds, firstorg=None, firstou=None, reporter=None):
451     """Extend Samba4 with OpenChange data.
452     
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)
459
460     If a progress reporter is not provided, a text output reporter is provided
461     """
462     names = guess_names_from_smbconf(lp, firstorg, firstou)
463
464     print "NOTE: This operation can take several minutes"
465
466     if reporter is None:
467         reporter = TextProgressReporter()
468
469     # Install OpenChange-specific schemas
470     install_schemas(setup_path, names, lp, creds, reporter)
471
472
473 def openchangedb_provision(lp, firstorg=None, firstou=None, mapistore=None):
474     """Create the OpenChange database.
475
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)
480     """
481     names = guess_names_from_smbconf(lp, firstorg, firstou)
482     
483     print "Setting up openchange db"
484     openchange_ldb = mailbox.OpenChangeDB(openchangedb_url(lp))
485     openchange_ldb.setup()
486
487     openchange_ldb.add_rootDSE(names.ocserverdn, names.firstorg, names.firstou)
488
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)
494
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)
499
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):
508                 return ret
509     # In source tree
510     ret = os.path.join(dirname, "../../setup")
511     if os.path.isdir(ret):
512         return ret
513     raise Exception("Unable to find setup directory.")