~ubuntu-branches/ubuntu/utopic/calendarserver/utopic

« back to all changes in this revision

Viewing changes to twistedcaldav/upgrade.py

  • Committer: Package Import Robot
  • Author(s): Rahul Amaram
  • Date: 2012-05-29 18:12:12 UTC
  • mfrom: (1.1.2)
  • Revision ID: package-import@ubuntu.com-20120529181212-mxjdfncopy6vou0f
Tags: 3.2+dfsg-1
* New upstream release
* Moved from using cdbs to dh sequencer
* Modified calenderserver init.d script based on /etc/init.d/skeleton script
* Removed ldapdirectory.patch as the OpenLDAP directory service has been 
  merged upstream
* Moved package to section "net" as calendarserver is more service than 
  library (Closes: #665859)
* Changed Architecture of calendarserver package to any as the package
  now includes compiled architecture dependent Python extensions
* Unowned files are no longer left on the system upon purging
  (Closes: #668731)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- test-case-name: twistedcaldav.test.test_upgrade -*-
2
2
##
3
 
# Copyright (c) 2008 Apple Inc. All rights reserved.
 
3
# Copyright (c) 2008-2010 Apple Inc. All rights reserved.
4
4
#
5
5
# Licensed under the Apache License, Version 2.0 (the "License");
6
6
# you may not use this file except in compliance with the License.
17
17
 
18
18
from __future__ import with_statement
19
19
 
20
 
from twisted.web2.dav.fileop import rmdir
21
 
from twisted.web2.dav import davxml
22
 
from twistedcaldav.directory.directory import DirectoryService
23
 
from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
 
20
import xattr, os, zlib, hashlib, datetime, pwd, grp, shutil, errno
 
21
from zlib import compress
 
22
from cPickle import loads as unpickle, UnpicklingError
 
23
 
 
24
from twext.web2.dav.fileop import rmdir
 
25
from twext.web2.dav import davxml
 
26
from twext.python.log import Logger
 
27
from twisted.python.reflect import namedClass
 
28
 
 
29
 
 
30
from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
 
31
from twistedcaldav.directory.xmlfile import XMLDirectoryService
 
32
from twistedcaldav.directory.directory import DirectoryService, GroupMembershipCacheUpdater
 
33
from twistedcaldav.directory import calendaruserproxy
 
34
from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
24
35
from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
25
36
from twistedcaldav.mail import MailGatewayTokensDatabase
26
 
from twistedcaldav.log import Logger
27
37
from twistedcaldav.ical import Component
28
38
from twistedcaldav import caldavxml
 
39
from twistedcaldav.scheduling.cuaddress import LocalCalendarUser
 
40
from twistedcaldav.scheduling.scheduler import DirectScheduler
 
41
 
 
42
 
 
43
from twisted.application.service import Service
 
44
from twisted.internet import reactor
 
45
from twisted.internet.defer import inlineCallbacks, succeed, returnValue
 
46
 
 
47
from txdav.caldav.datastore.index_file import db_basename
 
48
 
 
49
from calendarserver.tap.util import getRootResource, FakeRequest, directoryFromConfig
 
50
 
29
51
from calendarserver.tools.util import getDirectory
30
 
import xattr, os, zlib, hashlib, datetime, pwd, grp, shutil
31
 
from zlib import compress
32
 
from cPickle import loads as unpickle, UnpicklingError
33
 
 
 
52
from calendarserver.tools.resources import migrateResources
 
53
 
 
54
from twisted.python.reflect import namedAny
 
55
 
 
56
deadPropertyXattrPrefix = namedAny(
 
57
    "txdav.base.propertystore.xattr.PropertyStore.deadPropertyXattrPrefix"
 
58
)
 
59
 
 
60
INBOX_ITEMS = "inboxitems.txt"
34
61
 
35
62
log = Logger()
36
63
 
 
64
def xattrname(n):
 
65
    return deadPropertyXattrPrefix + n
 
66
 
37
67
def getCalendarServerIDs(config):
38
68
 
39
69
    # Determine uid/gid for ownership of directories we create here
59
89
# Upconverts data from any calendar server version prior to data format 1
60
90
#
61
91
 
62
 
def upgrade_to_1(config):
 
92
@inlineCallbacks
 
93
def upgrade_to_1(config, directory):
63
94
 
64
95
    errorOccurred = False
65
96
 
84
115
 
85
116
 
86
117
 
87
 
    def normalizeCUAddrs(data, directory):
88
 
        cal = Component.fromString(data)
89
 
 
90
 
        def lookupFunction(cuaddr):
91
 
            try:
92
 
                principal = directory.principalForCalendarUserAddress(cuaddr)
93
 
            except Exception, e:
94
 
                log.debug("Lookup of %s failed: %s" % (cuaddr, e))
95
 
                principal = None
96
 
 
97
 
            if principal is None:
98
 
                return (None, None, None)
99
 
            else:
100
 
                return (principal.record.fullName.decode("utf-8"),
101
 
                    principal.record.guid,
102
 
                    principal.record.calendarUserAddresses)
103
 
 
104
 
        cal.normalizeCalendarUserAddresses(lookupFunction)
105
 
 
106
 
        newData = str(cal)
107
 
        return newData, not newData == data
108
 
 
109
 
 
110
 
    def upgradeCalendarCollection(calPath, directory):
 
118
 
 
119
 
 
120
    def upgradeCalendarCollection(calPath, directory, cuaCache):
111
121
 
112
122
        errorOccurred = False
113
123
        collectionUpdated = False
140
150
                    continue
141
151
 
142
152
                try:
143
 
                    data, fixed = normalizeCUAddrs(data, directory)
 
153
                    data, fixed = removeIllegalCharacters(data)
 
154
                    if fixed:
 
155
                        log.warn("Removing illegal characters in %s" % (resPath,))
 
156
                        needsRewrite = True
 
157
                except Exception, e:
 
158
                    log.error("Error while removing illegal characters in %s: %s" %
 
159
                        (resPath, e))
 
160
                    errorOccurred = True
 
161
                    continue
 
162
 
 
163
                try:
 
164
                    data, fixed = normalizeCUAddrs(data, directory, cuaCache)
144
165
                    if fixed:
145
166
                        log.debug("Normalized CUAddrs in %s" % (resPath,))
146
167
                        needsRewrite = True
156
177
 
157
178
                md5value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<getcontentmd5 xmlns='http://twistedmatrix.com/xml_namespace/dav/'>%s</getcontentmd5>\r\n" % (hashlib.md5(data).hexdigest(),)
158
179
                md5value = zlib.compress(md5value)
159
 
                xattr.setxattr(resPath, "WebDAV:{http:%2F%2Ftwistedmatrix.com%2Fxml_namespace%2Fdav%2F}getcontentmd5", md5value)
 
180
                try:
 
181
                    xattr.setxattr(resPath, xattrname("{http:%2F%2Ftwistedmatrix.com%2Fxml_namespace%2Fdav%2F}getcontentmd5"), md5value)
 
182
                except IOError, ioe:
 
183
                    if ioe.errno == errno.EOPNOTSUPP:
 
184
                        # On non-native xattr systems we cannot do this,
 
185
                        # but those systems will typically not be migrating
 
186
                        # from pre-v1
 
187
                        pass
 
188
                except:
 
189
                    raise
160
190
 
161
191
                collectionUpdated = True
162
192
 
164
194
        if collectionUpdated:
165
195
            ctagValue = "<?xml version='1.0' encoding='UTF-8'?>\r\n<getctag xmlns='http://calendarserver.org/ns/'>%s</getctag>\r\n" % (str(datetime.datetime.now()),)
166
196
            ctagValue = zlib.compress(ctagValue)
167
 
            xattr.setxattr(calPath, "WebDAV:{http:%2F%2Fcalendarserver.org%2Fns%2F}getctag", ctagValue)
 
197
            try:
 
198
                xattr.setxattr(calPath, xattrname("{http:%2F%2Fcalendarserver.org%2Fns%2F}getctag"), ctagValue)
 
199
            except IOError, ioe:
 
200
                if ioe.errno == errno.EOPNOTSUPP:
 
201
                    # On non-native xattr systems we cannot do this,
 
202
                    # but those systems will typically not be migrating
 
203
                    # from pre-v1
 
204
                    pass
 
205
            except:
 
206
                raise
168
207
 
169
208
        return errorOccurred
170
209
 
171
210
 
172
 
    def upgradeCalendarHome(homePath, directory):
 
211
    def upgradeCalendarHome(homePath, directory, cuaCache):
173
212
 
174
213
        errorOccurred = False
175
214
 
187
226
                    rmdir(calPath)
188
227
                    continue
189
228
                log.debug("Upgrading calendar: %s" % (calPath,))
190
 
                if not upgradeCalendarCollection(calPath, directory):
 
229
                if not upgradeCalendarCollection(calPath, directory, cuaCache):
191
230
                    errorOccurred = True
192
231
 
193
232
                # Change the calendar-free-busy-set xattrs of the inbox to the
194
233
                # __uids__/<guid> form
195
234
                if cal == "inbox":
196
 
                    for attr, value in xattr.xattr(calPath).iteritems():
197
 
                        if attr == "WebDAV:{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set":
198
 
                            value = updateFreeBusySet(value, directory)
199
 
                            if value is not None:
200
 
                                # Need to write the xattr back to disk
201
 
                                xattr.setxattr(calPath, attr, value)
 
235
                    try:
 
236
                        for attr, value in xattr.xattr(calPath).iteritems():
 
237
                            if attr == xattrname("{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set"):
 
238
                                value = updateFreeBusySet(value, directory)
 
239
                                if value is not None:
 
240
                                    # Need to write the xattr back to disk
 
241
                                    xattr.setxattr(calPath, attr, value)
 
242
                    except IOError, ioe:
 
243
                        if ioe.errno == errno.EOPNOTSUPP:
 
244
                            # On non-native xattr systems we cannot do this,
 
245
                            # but those systems will typically not be migrating
 
246
                            # from pre-v1
 
247
                            pass
 
248
                    except:
 
249
                        raise
 
250
 
202
251
 
203
252
        except Exception, e:
204
253
            log.error("Failed to upgrade calendar home %s: %s" % (homePath, e))
208
257
 
209
258
 
210
259
    def doProxyDatabaseMoveUpgrade(config, uid=-1, gid=-1):
211
 
 
212
260
        # See if the new one is already present
213
 
        newDbPath = os.path.join(config.DataRoot,
214
 
            CalendarUserProxyDatabase.dbFilename)
 
261
        oldFilename = ".db.calendaruserproxy"
 
262
        newFilename = "proxies.sqlite"
 
263
 
 
264
        newDbPath = os.path.join(config.DataRoot, newFilename)
215
265
        if os.path.exists(newDbPath):
216
266
            # Nothing to be done, it's already in the new location
217
267
            return
218
268
 
219
269
        # See if the old DB is present
220
 
        oldDbPath = os.path.join(config.DocumentRoot, "principals",
221
 
            CalendarUserProxyDatabase.dbOldFilename)
 
270
        oldDbPath = os.path.join(config.DocumentRoot, "principals", oldFilename)
222
271
        if not os.path.exists(oldDbPath):
223
272
            # Nothing to be moved
224
273
            return
260
309
        os.rename(oldHome, newHome)
261
310
 
262
311
 
 
312
    @inlineCallbacks
263
313
    def migrateResourceInfo(config, directory, uid, gid):
 
314
        """
 
315
        Retrieve delegate assignments and auto-schedule flag from the directory
 
316
        service, because in "v1" that's where this info lived.
 
317
        """
 
318
 
264
319
        log.info("Fetching delegate assignments and auto-schedule settings from directory")
265
 
        resourceInfoDatabase = ResourceInfoDatabase(config.DataRoot)
266
 
        calendarUserProxyDatabase = CalendarUserProxyDatabase(config.DataRoot)
267
320
        resourceInfo = directory.getResourceInfo()
 
321
        if len(resourceInfo) == 0:
 
322
            # Nothing to migrate, or else not appleopendirectory
 
323
            return
 
324
 
 
325
        resourceInfoDatabase = ResourceInfoDatabase(config.DataRoot)
 
326
        proxydbClass = namedClass(config.ProxyDBService.type)
 
327
        calendarUserProxyDatabase = proxydbClass(**config.ProxyDBService.params)
 
328
 
268
329
        for guid, autoSchedule, proxy, readOnlyProxy in resourceInfo:
269
330
            resourceInfoDatabase.setAutoScheduleInDatabase(guid, autoSchedule)
270
331
            if proxy:
271
 
                calendarUserProxyDatabase.setGroupMembersInDatabase(
 
332
                yield calendarUserProxyDatabase.setGroupMembersInDatabase(
272
333
                    "%s#calendar-proxy-write" % (guid,),
273
334
                    [proxy]
274
335
                )
275
336
            if readOnlyProxy:
276
 
                calendarUserProxyDatabase.setGroupMembersInDatabase(
 
337
                yield calendarUserProxyDatabase.setGroupMembersInDatabase(
277
338
                    "%s#calendar-proxy-read" % (guid,),
278
339
                    [readOnlyProxy]
279
340
                )
282
343
        if os.path.exists(dbPath):
283
344
            os.chown(dbPath, uid, gid)
284
345
 
285
 
        dbPath = os.path.join(config.DataRoot, CalendarUserProxyDatabase.dbFilename)
 
346
        dbPath = os.path.join(config.DataRoot, "proxies.sqlite")
286
347
        if os.path.exists(dbPath):
287
348
            os.chown(dbPath, uid, gid)
288
349
 
 
350
 
289
351
    def createMailTokensDatabase(config, uid, gid):
290
352
        # Cause the tokens db to be created on disk so we can set the
291
353
        # permissions on it now
295
357
        if os.path.exists(dbPath):
296
358
            os.chown(dbPath, uid, gid)
297
359
 
298
 
    def createTaskServiceDirectory(config, uid, gid):
299
 
 
300
 
        taskDir = os.path.join(config.DataRoot, "tasks")
301
 
        if not os.path.exists(taskDir):
302
 
            os.mkdir(taskDir)
303
 
        os.chown(taskDir, uid, gid)
304
 
 
305
 
        incomingDir = os.path.join(taskDir, "incoming")
306
 
        if not os.path.exists(incomingDir):
307
 
            os.mkdir(incomingDir)
308
 
        os.chown(incomingDir, uid, gid)
309
 
 
310
 
        return incomingDir
311
 
 
312
 
 
313
 
 
314
 
    directory = getDirectory()
 
360
        journalPath = "%s-journal" % (dbPath,)
 
361
        if os.path.exists(journalPath):
 
362
            os.chown(journalPath, uid, gid)
 
363
 
 
364
 
 
365
 
 
366
    cuaCache = {}
315
367
 
316
368
    docRoot = config.DocumentRoot
317
369
 
398
450
                        if len(second) == 2:
399
451
                            secondPath = os.path.join(firstPath, second)
400
452
                            for home in os.listdir(secondPath):
 
453
                                homePath = os.path.join(secondPath, home)
 
454
                                if not os.path.isdir(homePath):
 
455
                                    # Skip non-directories
 
456
                                    continue
401
457
                                total += 1
402
 
                                homePath = os.path.join(secondPath, home)
403
458
                                inboxPath = os.path.join(homePath, "inbox")
404
459
                                if os.path.exists(inboxPath):
405
460
                                    for inboxItem in os.listdir(inboxPath):
406
461
                                        if not inboxItem.startswith("."):
407
462
                                            inboxItems.add(os.path.join(inboxPath, inboxItem))
408
463
 
409
 
            incomingDir = createTaskServiceDirectory(config, uid, gid)
410
464
            if inboxItems:
411
 
                taskFile = os.path.join(incomingDir, "scheduleinboxes.task")
412
 
                with open(taskFile, "w") as out:
 
465
                inboxItemsFile = os.path.join(config.DataRoot, INBOX_ITEMS)
 
466
                with open(inboxItemsFile, "w") as out:
413
467
                    for item in inboxItems:
414
468
                        out.write("%s\n" % (item))
415
 
                os.chown(taskFile, uid, gid)
 
469
                os.chown(inboxItemsFile, uid, gid)
416
470
 
417
471
            if total:
418
472
                log.warn("Processing %d calendar homes in %s" % (total, uidHomes))
428
482
                                for home in os.listdir(secondPath):
429
483
                                    homePath = os.path.join(secondPath, home)
430
484
 
 
485
                                    if not os.path.isdir(homePath):
 
486
                                        # Skip non-directories
 
487
                                        continue
431
488
 
432
489
                                    if not upgradeCalendarHome(homePath,
433
 
                                        directory):
 
490
                                        directory, cuaCache):
434
491
                                        errorOccurred = True
435
492
 
436
493
                                    count += 1
440
497
 
441
498
                log.warn("Done processing calendar homes")
442
499
 
443
 
    migrateResourceInfo(config, directory, uid, gid)
 
500
    yield migrateResourceInfo(config, directory, uid, gid)
444
501
    createMailTokensDatabase(config, uid, gid)
445
502
 
446
503
    if errorOccurred:
447
504
        raise UpgradeError("Data upgrade failed, see error.log for details")
448
505
 
449
506
 
 
507
def normalizeCUAddrs(data, directory, cuaCache):
 
508
    """
 
509
    Normalize calendar user addresses to urn:uuid: form.
 
510
 
 
511
    @param data: the calendar data to convert
 
512
    @type data: C{str}
 
513
    @param directory: the directory service to lookup CUAs with
 
514
    @type data: L{DirectoryService}
 
515
    @param cuaCache: the dictionary to use as a cache across calls, which is
 
516
        updated as a side-effect
 
517
    @type cuaCache: C{dict}
 
518
    @return: tuple of (converted calendar data, boolean signaling whether
 
519
        there were any changes to the data)
 
520
    """
 
521
    cal = Component.fromString(data)
 
522
 
 
523
    def lookupFunction(cuaddr):
 
524
 
 
525
        # Return cached results, if any.
 
526
        if cuaCache.has_key(cuaddr):
 
527
            return cuaCache[cuaddr]
 
528
 
 
529
        try:
 
530
            principal = directory.principalForCalendarUserAddress(cuaddr)
 
531
        except Exception, e:
 
532
            log.debug("Lookup of %s failed: %s" % (cuaddr, e))
 
533
            principal = None
 
534
 
 
535
        if principal is None:
 
536
            result = (None, None, None)
 
537
        else:
 
538
            rec = principal.record
 
539
 
 
540
            # RFC5545 syntax does not allow backslash escaping in
 
541
            # parameter values. A double-quote is thus not allowed
 
542
            # in a parameter value except as the start/end delimiters.
 
543
            # Single quotes are allowed, so we convert any double-quotes
 
544
            # to single-quotes.
 
545
            fullName = rec.fullName.replace('"', "'")
 
546
 
 
547
            result = (fullName, rec.guid, rec.calendarUserAddresses)
 
548
 
 
549
        # Cache the result
 
550
        cuaCache[cuaddr] = result
 
551
        return result
 
552
 
 
553
    cal.normalizeCalendarUserAddresses(lookupFunction)
 
554
 
 
555
    newData = str(cal)
 
556
    return newData, not newData == data
 
557
 
 
558
 
 
559
 
 
560
@inlineCallbacks
 
561
def upgrade_to_2(config, directory):
 
562
    
 
563
    errorOccurred = False
 
564
 
 
565
    def renameProxyDB():
 
566
        #
 
567
        # Rename proxy DB
 
568
        #
 
569
        oldFilename = "calendaruserproxy.sqlite"
 
570
        newFilename = "proxies.sqlite"
 
571
    
 
572
        oldDbPath = os.path.join(config.DataRoot, oldFilename)
 
573
        newDbPath = os.path.join(config.DataRoot, newFilename)
 
574
        if os.path.exists(oldDbPath) and not os.path.exists(newDbPath):
 
575
            os.rename(oldDbPath, newDbPath)
 
576
 
 
577
    def flattenHome(calHome):
 
578
 
 
579
        log.debug("Flattening calendar home: %s" % (calHome,))
 
580
 
 
581
        try:
 
582
            for cal in os.listdir(calHome):
 
583
                calPath = os.path.join(calHome, cal)
 
584
                if not os.path.isdir(calPath):
 
585
                    # Skip non-directories; these might have been uploaded by a
 
586
                    # random DAV client, they can't be calendar collections.
 
587
                    continue
 
588
                
 
589
                if cal in ("dropbox",):
 
590
                    continue
 
591
                if os.path.exists(os.path.join(calPath, db_basename)):
 
592
                    continue
 
593
                
 
594
                # Commented this out because it is only needed if there are calendars nested inside of regular collections.
 
595
                # Whilst this is technically possible in early versions of the servers the main clients did not support it.
 
596
                # Also, the v1 upgrade does not look at nested calendars for cu-address normalization.
 
597
                # However, we do still need to "ignore" regular collections in the calendar home so what we do is rename them
 
598
                # with a ".collection." prefix.
 
599
#                def scanCollection(collection):
 
600
#                    
 
601
#                    for child in os.listdir(collection):
 
602
#                        childCollection = os.path.join(collection, child)
 
603
#                        if os.path.isdir(childCollection):
 
604
#                            if os.path.exists(os.path.join(childCollection, db_basename)):
 
605
#                                newPath = os.path.join(calHome, child)
 
606
#                                if os.path.exists(newPath):
 
607
#                                    newPath = os.path.join(calHome, str(uuid.uuid4()))
 
608
#                                log.debug("Moving up calendar: %s" % (childCollection,))
 
609
#                                os.rename(childCollection, newPath)
 
610
#                            else:
 
611
#                                scanCollection(childCollection)
 
612
 
 
613
                if os.path.isdir(calPath):
 
614
                    #log.debug("Regular collection scan: %s" % (calPath,))
 
615
                    #scanCollection(calPath)
 
616
                    log.warn("Regular collection hidden: %s" % (calPath,))
 
617
                    os.rename(calPath, os.path.join(calHome, ".collection." + os.path.basename(calPath)))
 
618
 
 
619
        except Exception, e:
 
620
            log.error("Failed to upgrade calendar home %s: %s" % (calHome, e))
 
621
            return succeed(False)
 
622
        
 
623
        return succeed(True)
 
624
 
 
625
    def flattenHomes():
 
626
        """
 
627
        Make sure calendars inside regular collections are all moved to the top level.
 
628
        """
 
629
        errorOccurred = False
 
630
    
 
631
        log.debug("Flattening calendar homes")
 
632
 
 
633
        docRoot = config.DocumentRoot
 
634
        if os.path.exists(docRoot):
 
635
            calRoot = os.path.join(docRoot, "calendars")
 
636
            if os.path.exists(calRoot) and os.path.isdir(calRoot):
 
637
                uidHomes = os.path.join(calRoot, "__uids__")
 
638
                if os.path.isdir(uidHomes): 
 
639
                    for path1 in os.listdir(uidHomes):
 
640
                        uidLevel1 = os.path.join(uidHomes, path1)
 
641
                        if not os.path.isdir(uidLevel1):
 
642
                            continue
 
643
                        for path2 in os.listdir(uidLevel1):
 
644
                            uidLevel2 = os.path.join(uidLevel1, path2)
 
645
                            if not os.path.isdir(uidLevel2):
 
646
                                continue
 
647
                            for home in os.listdir(uidLevel2):
 
648
                                calHome = os.path.join(uidLevel2, home)
 
649
                                if not os.path.isdir(calHome):
 
650
                                    continue
 
651
                                if not flattenHome(calHome):
 
652
                                    errorOccurred = True
 
653
 
 
654
        return errorOccurred
 
655
 
 
656
    renameProxyDB()
 
657
 
 
658
    # Move auto-schedule from resourceinfo sqlite to augments:
 
659
    yield migrateAutoSchedule(config, directory)
 
660
 
 
661
    errorOccurred = flattenHomes()
 
662
 
 
663
    if errorOccurred:
 
664
        raise UpgradeError("Data upgrade failed, see error.log for details")
 
665
 
 
666
 
450
667
# The on-disk version number (which defaults to zero if .calendarserver_version
451
668
# doesn't exist), is compared with each of the numbers in the upgradeMethods
452
669
# array.  If it is less than the number, the associated method is called.
453
670
 
454
671
upgradeMethods = [
455
672
    (1, upgrade_to_1),
 
673
    (2, upgrade_to_2),
456
674
]
457
675
 
 
676
@inlineCallbacks
458
677
def upgradeData(config):
459
678
 
 
679
    directory = getDirectory()
 
680
 
 
681
    try:
 
682
        # Migrate locations/resources now because upgrade_to_1 depends on them
 
683
        # being in resources.xml
 
684
        (yield migrateFromOD(config, directory))
 
685
    except Exception, e:
 
686
        raise UpgradeError("Unable to migrate locations and resources from OD: %s" % (e,))
 
687
 
460
688
    docRoot = config.DocumentRoot
461
689
 
462
690
    versionFilePath = os.path.join(docRoot, ".calendarserver_version")
466
694
        try:
467
695
            with open(versionFilePath) as versionFile:
468
696
                onDiskVersion = int(versionFile.read().strip())
469
 
        except IOError, e:
 
697
        except IOError:
470
698
            log.error("Cannot open %s; skipping migration" %
471
699
                (versionFilePath,))
472
 
        except ValueError, e:
 
700
        except ValueError:
473
701
            log.error("Invalid version number in %s; skipping migration" %
474
702
                (versionFilePath,))
475
703
 
478
706
    for version, method in upgradeMethods:
479
707
        if onDiskVersion < version:
480
708
            log.warn("Upgrading to version %d" % (version,))
481
 
            method(config)
 
709
            (yield method(config, directory))
 
710
            log.warn("Upgraded to version %d" % (version,))
482
711
            with open(versionFilePath, "w") as verFile:
483
712
                verFile.write(str(version))
484
713
            os.chown(versionFilePath, uid, gid)
485
714
 
486
 
 
487
715
class UpgradeError(RuntimeError):
488
716
    """
489
717
    Generic upgrade error.
595
823
        # Can't rename, must copy/delete
596
824
        shutil.copy2(srcPath, destPath)
597
825
        os.remove(srcPath)
 
826
 
 
827
 
 
828
DELETECHARS = ''.join(chr(i) for i in xrange(32) if i not in (10, 13))
 
829
def removeIllegalCharacters(data):
 
830
    """
 
831
    Remove all characters below ASCII 32 except NL and CR
 
832
 
 
833
    Return tuple with the processed data, and a boolean indicating wether
 
834
    the data changed.
 
835
    """
 
836
    beforeLen = len(data)
 
837
    data =  data.translate(None, DELETECHARS)
 
838
    afterLen = len(data)
 
839
    if afterLen != beforeLen:
 
840
        return data, True
 
841
    else:
 
842
        return data, False
 
843
 
 
844
 
 
845
# Deferred
 
846
def migrateFromOD(config, directory):
 
847
    #
 
848
    # Migrates locations and resources from OD
 
849
    #
 
850
    triggerFile = "trigger_resource_migration"
 
851
    triggerPath = os.path.join(config.ServerRoot, triggerFile)
 
852
    if os.path.exists(triggerPath):
 
853
        os.remove(triggerPath)
 
854
 
 
855
        log.warn("Migrating locations and resources")
 
856
 
 
857
        userService = directory.serviceForRecordType("users")
 
858
        resourceService = directory.serviceForRecordType("resources")
 
859
        if (
 
860
            not isinstance(userService, OpenDirectoryService) or
 
861
            not isinstance(resourceService, XMLDirectoryService)
 
862
        ):
 
863
            # Configuration requires no migration
 
864
            return succeed(None)
 
865
 
 
866
        # Create internal copies of resources and locations based on what is
 
867
        # found in OD
 
868
        return migrateResources(userService, resourceService)
 
869
 
 
870
 
 
871
@inlineCallbacks
 
872
def migrateAutoSchedule(config, directory):
 
873
    # Fetch the autoSchedule assignments from resourceinfo.sqlite and store
 
874
    # the values in augments
 
875
    augmentService = directory.augmentService
 
876
    augmentRecords = []
 
877
    dbPath = os.path.join(config.DataRoot, ResourceInfoDatabase.dbFilename)
 
878
    if os.path.exists(dbPath):
 
879
        resourceInfoDatabase = ResourceInfoDatabase(config.DataRoot)
 
880
        results = resourceInfoDatabase._db_execute(
 
881
            "select GUID, AUTOSCHEDULE from RESOURCEINFO"
 
882
        )
 
883
        for guid, autoSchedule in results:
 
884
            record = directory.recordWithGUID(guid)
 
885
            if record is not None:
 
886
                augmentRecord = (yield augmentService.getAugmentRecord(guid, record.recordType))
 
887
                augmentRecord.autoSchedule = autoSchedule
 
888
                augmentRecords.append(augmentRecord)
 
889
 
 
890
    yield augmentService.addAugmentRecords(augmentRecords)
 
891
 
 
892
 
 
893
class UpgradeFileSystemFormatService(Service, object):
 
894
    """
 
895
    Upgrade filesystem from previous versions.
 
896
    """
 
897
 
 
898
    def __init__(self, config, service):
 
899
        """
 
900
        Initialize the service.
 
901
        """
 
902
        self.wrappedService = service
 
903
        self.config = config
 
904
 
 
905
 
 
906
    @inlineCallbacks
 
907
    def doUpgrade(self):
 
908
        """
 
909
        Do the upgrade.  Called by C{startService}, but a different method
 
910
        because C{startService} should return C{None}, not a L{Deferred}.
 
911
 
 
912
        @return: a Deferred which fires when the upgrade is complete.
 
913
        """
 
914
 
 
915
        # Don't try to use memcached during upgrade; it's not necessarily
 
916
        # running yet.
 
917
        memcacheEnabled = self.config.Memcached.Pools.Default.ClientEnabled
 
918
        self.config.Memcached.Pools.Default.ClientEnabled = False
 
919
 
 
920
        yield upgradeData(self.config)
 
921
 
 
922
        # Restore memcached client setting
 
923
        self.config.Memcached.Pools.Default.ClientEnabled = memcacheEnabled
 
924
 
 
925
        # see http://twistedmatrix.com/trac/ticket/4649
 
926
        reactor.callLater(0, self.wrappedService.setServiceParent, self.parent)
 
927
 
 
928
 
 
929
    def startService(self):
 
930
        """
 
931
        Start the service.
 
932
        """
 
933
        self.doUpgrade()
 
934
 
 
935
 
 
936
 
 
937
class PostDBImportService(Service, object):
 
938
    """
 
939
    Service which runs after database import but before workers are spawned
 
940
    (except memcached will be running at this point)
 
941
 
 
942
    The jobs carried out here are:
 
943
 
 
944
        1. Populating the group-membership cache
 
945
        2. Processing non-implicit inbox items
 
946
    """
 
947
 
 
948
    def __init__(self, config, store, service):
 
949
        """
 
950
        Initialize the service.
 
951
        """
 
952
        self.wrappedService = service
 
953
        self.store = store
 
954
        self.config = config
 
955
 
 
956
    @inlineCallbacks
 
957
    def startService(self):
 
958
        """
 
959
        Start the service.
 
960
        """
 
961
 
 
962
        # Load proxy assignments from XML if specified
 
963
        if self.config.ProxyLoadFromFile:
 
964
            proxydbClass = namedClass(self.config.ProxyDBService.type)
 
965
            calendaruserproxy.ProxyDBService = proxydbClass(
 
966
                **self.config.ProxyDBService.params)
 
967
            loader = XMLCalendarUserProxyLoader(self.config.ProxyLoadFromFile)
 
968
            yield loader.updateProxyDB()
 
969
 
 
970
        # Populate the group membership cache
 
971
        if (self.config.GroupCaching.Enabled and
 
972
            self.config.GroupCaching.EnableUpdater):
 
973
            proxydb = calendaruserproxy.ProxyDBService
 
974
            if proxydb is None:
 
975
                proxydbClass = namedClass(self.config.ProxyDBService.type)
 
976
                proxydb = proxydbClass(**self.config.ProxyDBService.params)
 
977
            directory = directoryFromConfig(self.config)
 
978
 
 
979
            updater = GroupMembershipCacheUpdater(proxydb,
 
980
                directory, self.config.GroupCaching.ExpireSeconds,
 
981
                self.config.GroupCaching.LockSeconds,
 
982
                namespace=self.config.GroupCaching.MemcachedPool,
 
983
                useExternalProxies=self.config.GroupCaching.UseExternalProxies)
 
984
            yield updater.updateCache(fast=True)
 
985
 
 
986
            uid, gid = getCalendarServerIDs(self.config)
 
987
            dbPath = os.path.join(self.config.DataRoot, "proxies.sqlite")
 
988
            if os.path.exists(dbPath):
 
989
                os.chown(dbPath, uid, gid)
 
990
 
 
991
        # Process old inbox items
 
992
        yield self.processInboxItems()
 
993
 
 
994
 
 
995
    @inlineCallbacks
 
996
    def processInboxItems(self):
 
997
        """
 
998
        When data is migrated from a non-implicit scheduling server there can
 
999
        be inbox items that clients have not yet processed.  This method
 
1000
        runs those inbox items through the implicit scheduling mechanism.
 
1001
        """
 
1002
 
 
1003
        inboxItemsList = os.path.join(self.config.DataRoot, INBOX_ITEMS)
 
1004
        if os.path.exists(inboxItemsList):
 
1005
 
 
1006
            root = getRootResource(self.config, self.store)
 
1007
            directory = root.getDirectory()
 
1008
            principalCollection = directory.principalCollection
 
1009
 
 
1010
            inboxItems = set()
 
1011
            with open(inboxItemsList) as input:
 
1012
                for inboxItem in input:
 
1013
                    inboxItem = inboxItem.strip()
 
1014
                    inboxItems.add(inboxItem)
 
1015
 
 
1016
            try:
 
1017
                for inboxItem in list(inboxItems):
 
1018
                    log.info("Processing inbox item: %s" % (inboxItem,))
 
1019
                    ignore, uuid, ignore, fileName = inboxItem.rsplit("/", 3)
 
1020
 
 
1021
                    record = directory.recordWithUID(uuid)
 
1022
                    if not record:
 
1023
                        continue
 
1024
 
 
1025
                    principal = principalCollection.principalForRecord(record)
 
1026
                    if not principal:
 
1027
                        continue
 
1028
 
 
1029
                    request = FakeRequest(root, "PUT", None)
 
1030
                    request.checkedSACL = True
 
1031
                    request.authnUser = request.authzUser = davxml.Principal(
 
1032
                        davxml.HRef.fromString("/principals/__uids__/%s/" % (uuid,))
 
1033
                    )
 
1034
 
 
1035
                    calendarHome = yield principal.calendarHome(request)
 
1036
                    if not calendarHome:
 
1037
                        continue
 
1038
 
 
1039
                    inbox = yield calendarHome.getChild("inbox")
 
1040
                    if inbox and inbox.exists():
 
1041
 
 
1042
                        inboxItemResource = yield inbox.getChild(fileName)
 
1043
                        if inboxItemResource and inboxItemResource.exists():
 
1044
 
 
1045
                            uri = "/calendars/__uids__/%s/inbox/%s" % (uuid,
 
1046
                                fileName)
 
1047
                            request.path = uri
 
1048
                            request._rememberResource(inboxItemResource, uri)
 
1049
 
 
1050
                            try:
 
1051
                                yield self.processInboxItem(
 
1052
                                    root,
 
1053
                                    directory,
 
1054
                                    principal,
 
1055
                                    request,
 
1056
                                    inbox,
 
1057
                                    inboxItemResource,
 
1058
                                    uuid,
 
1059
                                    uri
 
1060
                                )
 
1061
                            except Exception, e:
 
1062
                                log.error("Error processing inbox item: %s (%s)"
 
1063
                                    % (inboxItem, e))
 
1064
 
 
1065
                    inboxItems.remove(inboxItem)
 
1066
 
 
1067
 
 
1068
            finally:
 
1069
                # Rewrite the inbox items file in case we exit before we're
 
1070
                # done so we'll pick up where we left off next time we start up.
 
1071
                if inboxItems:
 
1072
                    with open(inboxItemsList + ".tmp", "w") as output:
 
1073
                        for inboxItem in inboxItems:
 
1074
                            output.write("%s\n" % (inboxItem,))
 
1075
                    os.rename(inboxItemsList + ".tmp", inboxItemsList)
 
1076
                    log.error("Restart calendar service to reattempt processing")
 
1077
                else:
 
1078
                    os.remove(inboxItemsList)
 
1079
 
 
1080
        reactor.callLater(0, self.wrappedService.setServiceParent, self.parent)
 
1081
 
 
1082
 
 
1083
    @inlineCallbacks
 
1084
    def processInboxItem(self, root, directory, principal, request, inbox,
 
1085
        inboxItem, uuid, uri):
 
1086
        """
 
1087
        Run an individual inbox item through implicit scheduling and remove
 
1088
        the inbox item.
 
1089
        """
 
1090
 
 
1091
        log.debug("Processing inbox item %s" % (inboxItem,))
 
1092
 
 
1093
        txn = request._newStoreTransaction
 
1094
        txn._notifierFactory = None # Do not send push notifications
 
1095
 
 
1096
        ownerPrincipal = principal
 
1097
        cua = "urn:uuid:%s" % (uuid,)
 
1098
        owner = LocalCalendarUser(cua, ownerPrincipal,
 
1099
            inbox, ownerPrincipal.scheduleInboxURL())
 
1100
 
 
1101
        calendar = yield inboxItem.iCalendar()
 
1102
        if calendar.mainType() is not None:
 
1103
            try:
 
1104
                method = calendar.propertyValue("METHOD")
 
1105
            except ValueError:
 
1106
                returnValue(None)
 
1107
 
 
1108
            if method == "REPLY":
 
1109
                # originator is attendee sending reply
 
1110
                originator = calendar.getAttendees()[0]
 
1111
            else:
 
1112
                # originator is the organizer
 
1113
                originator = calendar.getOrganizer()
 
1114
 
 
1115
            principalCollection = directory.principalCollection
 
1116
            originatorPrincipal = principalCollection.principalForCalendarUserAddress(originator)
 
1117
            originator = LocalCalendarUser(originator, originatorPrincipal)
 
1118
            recipients = (owner,)
 
1119
 
 
1120
            scheduler = DirectScheduler(request, inboxItem)
 
1121
            # Process inbox item
 
1122
            yield scheduler.doSchedulingViaPUT(originator, recipients, calendar,
 
1123
                internal_request=False)
 
1124
        else:
 
1125
            log.warn("Removing invalid inbox item: %s" % (uri,))
 
1126
 
 
1127
        #
 
1128
        # Remove item
 
1129
        #
 
1130
        yield inboxItem.storeRemove(request, True, uri)
 
1131
        yield txn.commit()
 
1132