<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[14519] CalendarServer/branches/users/cdaboo/pod2pod-migration</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/14519">14519</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2015-03-06 12:13:58 -0800 (Fri, 06 Mar 2015)</dd>
</dl>
<h3>Log Message</h3>
<pre>Detailed migration logging. Command line tool to drive migration.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtwistedcaldavstdconfigpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/twistedcaldav/stdconfig.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationhome_syncpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationtesttest_home_syncpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationtesttest_migrationpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_migration.py</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationcalendarservertoolspod_migrationpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/calendarserver/tools/pod_migration.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationcalendarservertoolspod_migrationpy"></a>
<div class="addfile"><h4>Added: CalendarServer/branches/users/cdaboo/pod2pod-migration/calendarserver/tools/pod_migration.py (0 => 14519)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/calendarserver/tools/pod_migration.py         (rev 0)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/calendarserver/tools/pod_migration.py        2015-03-06 20:13:58 UTC (rev 14519)
</span><span class="lines">@@ -0,0 +1,293 @@
</span><ins>+#!/usr/bin/env python
+# -*- test-case-name: calendarserver.tools.test.test_calverify -*-
+##
+# Copyright (c) 2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from __future__ import print_function
+
+"""
+This tool manages an overall pod migration. Migration is done in a series of steps,
+with the system admin triggering each step individually by running this tool.
+"""
+
+import os
+import sys
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.text import wordWrap
+from twisted.python.usage import Options, UsageError
+
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+from twistedcaldav.timezones import TimezoneCache
+
+from txdav.common.datastore.podding.migration.home_sync import CrossPodHomeSync
+
+from twext.python.log import Logger
+from twext.who.idirectory import RecordType
+
+from calendarserver.tools.cmdline import utilityMain, WorkerService
+
+
+log = Logger()
+
+VERSION = "1"
+
+
+
+def usage(e=None):
+ if e:
+ print(e)
+ print("")
+ try:
+ PodMigrationOptions().opt_help()
+ except SystemExit:
+ pass
+ if e:
+ sys.exit(64)
+ else:
+ sys.exit(0)
+
+
+description = ''.join(
+ wordWrap(
+ """
+ Usage: calendarserver_pod_migration [options] [input specifiers]
+ """,
+ int(os.environ.get('COLUMNS', '80'))
+ )
+)
+description += "\nVersion: %s" % (VERSION,)
+
+
+
+class ConfigError(Exception):
+ pass
+
+
+
+class PodMigrationOptions(Options):
+ """
+ Command-line options for 'calendarserver_pod_migration'
+ """
+
+ synopsis = description
+
+ optFlags = [
+ ['verbose', 'v', "Verbose logging."],
+ ['debug', 'D', "Debug logging."],
+ ['step1', '1', "Run step 1 of the migration (initial sync)"],
+ ['step2', '2', "Run step 2 of the migration (incremental sync)"],
+ ['step3', '3', "Run step 3 of the migration (prepare for final sync)"],
+ ['step4', '4', "Run step 4 of the migration (final incremental sync)"],
+ ['step5', '5', "Run step 5 of the migration (final reconcile sync)"],
+ ['step6', '6', "Run step 6 of the migration (enable new home)"],
+ ['step7', '7', "Run step 7 of the migration (remove old home)"],
+ ]
+
+ optParameters = [
+ ['config', 'f', DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
+ ['uid', 'u', "", "Directory record uid of user to migrate [REQUIRED]"],
+ ]
+
+ longdesc = "Only one step option is allowed."
+
+ def __init__(self):
+ super(PodMigrationOptions, self).__init__()
+ self.outputName = '-'
+
+
+ def opt_output(self, filename):
+ """
+ Specify output file path (default: '-', meaning stdout).
+ """
+ self.outputName = filename
+
+ opt_o = opt_output
+
+
+ def openOutput(self):
+ """
+ Open the appropriate output file based on the '--output' option.
+ """
+ if self.outputName == '-':
+ return sys.stdout
+ else:
+ return open(self.outputName, 'wb')
+
+
+ def postOptions(self):
+ runstep = None
+ for step in range(7):
+ if self["step{}".format(step + 1)]:
+ if runstep is None:
+ runstep = step
+ self["runstep"] = step + 1
+ else:
+ raise UsageError("Only one step option allowed")
+ else:
+ if runstep is None:
+ raise UsageError("One step option must be present")
+ if not self["uid"]:
+ raise UsageError("A uid is required")
+
+
+
+class PodMigrationService(WorkerService, object):
+ """
+ Service which runs, does its stuff, then stops the reactor.
+ """
+
+ def __init__(self, store, options, output, reactor, config):
+ super(PodMigrationService, self).__init__(store)
+ self.options = options
+ self.output = output
+ self.reactor = reactor
+ self.config = config
+ TimezoneCache.create()
+
+
+ @inlineCallbacks
+ def doWork(self):
+ """
+ Do the work, stopping the reactor when done.
+ """
+ self.output.write("\n---- Pod Migration version: %s ----\n" % (VERSION,))
+
+ # Map short name to uid
+ record = yield self.store.directoryService().recordWithUID(self.options["uid"])
+ if record is None:
+ record = yield self.store.directoryService().recordWithShortName(RecordType.user, self.options["uid"])
+ if record is not None:
+ self.options["uid"] = record.uid
+
+ try:
+ yield getattr(self, "step{}".format(self.options["runstep"]))()
+ self.output.close()
+ except ConfigError:
+ pass
+ except:
+ log.failure("doWork()")
+
+
+ @inlineCallbacks
+ def step1(self):
+ syncer = CrossPodHomeSync(
+ self.store,
+ self.options["uid"],
+ uselog=self.output if self.options["verbose"] else None
+ )
+ syncer.accounting("Pod Migration Step 1\n")
+ yield syncer.sync()
+
+
+ @inlineCallbacks
+ def step2(self):
+ syncer = CrossPodHomeSync(
+ self.store,
+ self.options["uid"],
+ uselog=self.output if self.options["verbose"] else None
+ )
+ syncer.accounting("Pod Migration Step 2\n")
+ yield syncer.sync()
+
+
+ @inlineCallbacks
+ def step3(self):
+ syncer = CrossPodHomeSync(
+ self.store,
+ self.options["uid"],
+ uselog=self.output if self.options["verbose"] else None
+ )
+ syncer.accounting("Pod Migration Step 3\n")
+ yield syncer.disableRemoteHome()
+
+
+ @inlineCallbacks
+ def step4(self):
+ syncer = CrossPodHomeSync(
+ self.store,
+ self.options["uid"],
+ final=True,
+ uselog=self.output if self.options["verbose"] else None
+ )
+ syncer.accounting("Pod Migration Step 4\n")
+ yield syncer.sync()
+
+
+ @inlineCallbacks
+ def step5(self):
+ syncer = CrossPodHomeSync(
+ self.store,
+ self.options["uid"],
+ final=True,
+ uselog=self.output if self.options["verbose"] else None
+ )
+ syncer.accounting("Pod Migration Step 5\n")
+ yield syncer.finalSync()
+
+
+ @inlineCallbacks
+ def step6(self):
+ syncer = CrossPodHomeSync(
+ self.store,
+ self.options["uid"],
+ uselog=self.output if self.options["verbose"] else None
+ )
+ syncer.accounting("Pod Migration Step 6\n")
+ yield syncer.enableLocalHome()
+
+
+ @inlineCallbacks
+ def step7(self):
+ syncer = CrossPodHomeSync(
+ self.store,
+ self.options["uid"],
+ final=True,
+ uselog=self.output if self.options["verbose"] else None
+ )
+ syncer.accounting("Pod Migration Step 7\n")
+ yield syncer.removeRemoteHome()
+
+
+
+def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
+ """
+ Do the export.
+ """
+ if reactor is None:
+ from twisted.internet import reactor
+ options = PodMigrationOptions()
+ try:
+ options.parseOptions(argv[1:])
+ except UsageError as e:
+ stderr.write("Invalid options specified\n")
+ options.opt_help()
+
+ try:
+ output = options.openOutput()
+ except IOError, e:
+ stderr.write("Unable to open output file for writing: %s\n" % (e))
+ sys.exit(1)
+
+
+ def makeService(store):
+ from twistedcaldav.config import config
+ config.TransactionTimeoutSeconds = 0
+ return PodMigrationService(store, options, output, reactor, config)
+
+ utilityMain(options['config'], makeService, reactor, verbose=options["debug"])
+
+if __name__ == '__main__':
+ main()
</ins><span class="cx">Property changes on: CalendarServer/branches/users/cdaboo/pod2pod-migration/calendarserver/tools/pod_migration.py
</span><span class="cx">___________________________________________________________________
</span></span></pre></div>
<a id="svnexecutable"></a>
<div class="addfile"><h4>Added: svn:executable</h4></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtwistedcaldavstdconfigpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/twistedcaldav/stdconfig.py (14518 => 14519)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/twistedcaldav/stdconfig.py        2015-03-06 19:34:30 UTC (rev 14518)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/twistedcaldav/stdconfig.py        2015-03-06 20:13:58 UTC (rev 14519)
</span><span class="lines">@@ -405,6 +405,7 @@
</span><span class="cx"> "Implicit Errors": False,
</span><span class="cx"> "AutoScheduling": False,
</span><span class="cx"> "iSchedule": False,
</span><ins>+ "migration": False,
</ins><span class="cx"> },
</span><span class="cx"> "AccountingPrincipals": [],
</span><span class="cx"> "AccountingLogRoot" : "accounting",
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationhome_syncpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py (14518 => 14519)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py        2015-03-06 19:34:30 UTC (rev 14518)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py        2015-03-06 20:13:58 UTC (rev 14519)
</span><span class="lines">@@ -19,6 +19,7 @@
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from twisted.internet.defer import returnValue, inlineCallbacks
</span><span class="cx"> from twisted.python.failure import Failure
</span><ins>+from twistedcaldav.accounting import emitAccounting
</ins><span class="cx"> from txdav.caldav.icalendarstore import ComponentUpdateState
</span><span class="cx"> from txdav.common.datastore.podding.migration.sync_metadata import CalendarMigrationRecord, \
</span><span class="cx"> CalendarObjectMigrationRecord, AttachmentMigrationRecord
</span><span class="lines">@@ -30,9 +31,12 @@
</span><span class="cx"> from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
</span><span class="cx">
</span><span class="cx"> from uuid import uuid4
</span><ins>+import datetime
</ins><span class="cx">
</span><span class="cx"> log = Logger()
</span><span class="cx">
</span><ins>+ACCOUNTING_TYPE = "migration"
+ACCOUNTING_LOG = "migration.log"
</ins><span class="cx">
</span><span class="cx"> def inTransactionWrapper(operation):
</span><span class="cx"> """
</span><span class="lines">@@ -85,7 +89,7 @@
</span><span class="cx">
</span><span class="cx"> BATCH_SIZE = 50
</span><span class="cx">
</span><del>- def __init__(self, store, diruid, final=False):
</del><ins>+ def __init__(self, store, diruid, final=False, uselog=None):
</ins><span class="cx"> """
</span><span class="cx"> @param store: the data store
</span><span class="cx"> @type store: L{CommonDataStore}
</span><span class="lines">@@ -94,11 +98,14 @@
</span><span class="cx"> @param final: indicates whether this is in the final sync stage with the remote home
</span><span class="cx"> already disabled
</span><span class="cx"> @type final: L{bool}
</span><ins>+ @param uselog: additional logging written to this object
+ @type: L{File}
</ins><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> self.store = store
</span><span class="cx"> self.diruid = diruid
</span><span class="cx"> self.disabledRemote = final
</span><ins>+ self.uselog = uselog
</ins><span class="cx"> self.record = None
</span><span class="cx"> self.homeId = None
</span><span class="cx">
</span><span class="lines">@@ -107,6 +114,12 @@
</span><span class="cx"> return "Cross-pod Migration Sync for {}: {}".format(self.diruid, detail)
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def accounting(self, logstr):
+ emitAccounting(ACCOUNTING_TYPE, self.record, "{} {}\n".format(datetime.datetime.now().isoformat(), logstr), filename=ACCOUNTING_LOG)
+ if self.uselog is not None:
+ self.uselog.write("CrossPodHomeSync: {}\n".format(logstr))
+
+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def migrateHere(self):
</span><span class="cx"> """
</span><span class="lines">@@ -154,6 +167,7 @@
</span><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> yield self.loadRecord()
</span><ins>+ self.accounting("Starting: sync...")
</ins><span class="cx"> yield self.prepareCalendarHome()
</span><span class="cx">
</span><span class="cx"> # Calendar list and calendar data
</span><span class="lines">@@ -165,7 +179,9 @@
</span><span class="cx"> # Sync attachments
</span><span class="cx"> yield self.syncAttachments()
</span><span class="cx">
</span><ins>+ self.accounting("Completed: sync.\n")
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def finalSync(self):
</span><span class="cx"> """
</span><span class="lines">@@ -174,6 +190,7 @@
</span><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> yield self.loadRecord()
</span><ins>+ self.accounting("Starting: finalSync...")
</ins><span class="cx"> yield self.prepareCalendarHome()
</span><span class="cx">
</span><span class="cx"> # Link attachments to resources: ATTACHMENT_CALENDAR_OBJECT table
</span><span class="lines">@@ -198,7 +215,9 @@
</span><span class="cx"> # TODO: work items
</span><span class="cx"> pass
</span><span class="cx">
</span><ins>+ self.accounting("Completed: finalSync.\n")
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inTransactionWrapper
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def disableRemoteHome(self, txn):
</span><span class="lines">@@ -207,6 +226,7 @@
</span><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> yield self.loadRecord()
</span><ins>+ self.accounting("Starting: disableRemoteHome...")
</ins><span class="cx"> yield self.prepareCalendarHome()
</span><span class="cx">
</span><span class="cx"> # Calendar home
</span><span class="lines">@@ -219,7 +239,9 @@
</span><span class="cx">
</span><span class="cx"> self.disabledRemote = True
</span><span class="cx">
</span><ins>+ self.accounting("Completed: disableRemoteHome.\n")
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inTransactionWrapper
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def enableLocalHome(self, txn):
</span><span class="lines">@@ -228,6 +250,7 @@
</span><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> yield self.loadRecord()
</span><ins>+ self.accounting("Starting: enableLocalHome...")
</ins><span class="cx"> yield self.prepareCalendarHome()
</span><span class="cx">
</span><span class="cx"> # Disable any local external homes
</span><span class="lines">@@ -249,7 +272,9 @@
</span><span class="cx"> # TODO: purge the old ones
</span><span class="cx"> pass
</span><span class="cx">
</span><ins>+ self.accounting("Completed: enableLocalHome.\n")
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def removeRemoteHome(self):
</span><span class="cx"> """
</span><span class="lines">@@ -259,9 +284,12 @@
</span><span class="cx"> # TODO: implement API on CommonHome to purge the old data without
</span><span class="cx"> # any side-effects (scheduling, sharing etc).
</span><span class="cx"> yield self.loadRecord()
</span><ins>+ self.accounting("Starting: removeRemoteHome...")
</ins><span class="cx"> yield self.prepareCalendarHome()
</span><span class="cx">
</span><ins>+ self.accounting("Completed: removeRemoteHome.\n")
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def loadRecord(self):
</span><span class="cx"> """
</span><span class="lines">@@ -290,6 +318,7 @@
</span><span class="cx"> self.homeId = None
</span><span class="cx"> else:
</span><span class="cx"> home = yield txn.calendarHomeWithUID(self.diruid, status=_HOME_STATUS_MIGRATING, create=True)
</span><ins>+ self.accounting(" Created new home collection to migrate into.")
</ins><span class="cx"> self.homeId = home.id() if home is not None else None
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -300,6 +329,7 @@
</span><span class="cx"> Make sure the home meta-data (alarms, default calendars) is properly sync'd
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ self.accounting("Starting: syncCalendarHomeMetaData...")
</ins><span class="cx"> remote_home = yield self._remoteHome(txn)
</span><span class="cx"> yield remote_home.readMetaData()
</span><span class="cx">
</span><span class="lines">@@ -309,7 +339,9 @@
</span><span class="cx"> local_home = yield self._localHome(txn)
</span><span class="cx"> yield local_home.copyMetadata(remote_home, calendarIDMap)
</span><span class="cx">
</span><ins>+ self.accounting("Completed: syncCalendarHomeMetaData.")
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def _remoteHome(self, txn):
</span><span class="cx"> """
</span><span class="lines">@@ -350,11 +382,15 @@
</span><span class="cx"> Synchronize each owned calendar.
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ self.accounting("Starting: syncCalendarList...")
+
</ins><span class="cx"> # Remote sync details
</span><span class="cx"> remote_sync_state = yield self.getCalendarSyncList()
</span><ins>+ self.accounting(" Found {} remote calendars to sync.".format(len(remote_sync_state)))
</ins><span class="cx">
</span><span class="cx"> # Get local sync details from local DB
</span><span class="cx"> local_sync_state = yield self.getSyncState()
</span><ins>+ self.accounting(" Found {} local calendars to sync.".format(len(local_sync_state)))
</ins><span class="cx">
</span><span class="cx"> # Remove local calendars no longer on the remote side
</span><span class="cx"> yield self.purgeLocal(local_sync_state, remote_sync_state)
</span><span class="lines">@@ -363,7 +399,9 @@
</span><span class="cx"> for remoteID in remote_sync_state.keys():
</span><span class="cx"> yield self.syncCalendar(remoteID, local_sync_state, remote_sync_state)
</span><span class="cx">
</span><ins>+ self.accounting("Completed: syncCalendarList.")
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inTransactionWrapper
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def getCalendarSyncList(self, txn):
</span><span class="lines">@@ -433,11 +471,12 @@
</span><span class="cx"> @type remote_sync_state: L{dict}
</span><span class="cx"> """
</span><span class="cx"> home = yield self._localHome(txn)
</span><del>- for remoteID in set(local_sync_state.keys()) - set(remote_sync_state.keys()):
- calendar = yield home.childWithID(local_sync_state[remoteID].localResourceID)
</del><ins>+ for localID in set(local_sync_state.keys()) - set(remote_sync_state.keys()):
+ calendar = yield home.childWithID(local_sync_state[localID].localResourceID)
</ins><span class="cx"> if calendar is not None:
</span><span class="cx"> yield calendar.purge()
</span><del>- del local_sync_state[remoteID]
</del><ins>+ del local_sync_state[localID]
+ self.accounting(" Purged calendar local-id={} that no longer exists on the remote pod.".format(localID))
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -454,6 +493,8 @@
</span><span class="cx"> @type remote_sync_state: L{dict}
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ self.accounting("Starting: syncCalendar.")
+
</ins><span class="cx"> # See if we need to create the local one first
</span><span class="cx"> if remoteID not in local_sync_state:
</span><span class="cx"> localID = yield self.newCalendar()
</span><span class="lines">@@ -463,6 +504,10 @@
</span><span class="cx"> localResourceID=localID,
</span><span class="cx"> lastSyncToken=None,
</span><span class="cx"> )
</span><ins>+ self.accounting(" Created new calendar local-id={}, remote-id={}.".format(localID, remoteID))
+ else:
+ localID = local_sync_state.get(remoteID).localResourceID
+ self.accounting(" Updating calendar local-id={}, remote-id={}.".format(localID, remoteID))
</ins><span class="cx"> local_record = local_sync_state.get(remoteID)
</span><span class="cx">
</span><span class="cx"> remote_token = remote_sync_state[remoteID].lastSyncToken
</span><span class="lines">@@ -471,11 +516,13 @@
</span><span class="cx"> yield self.syncCalendarMetaData(local_record)
</span><span class="cx">
</span><span class="cx"> # Sync object resources
</span><del>- changed, deleted = yield self.findObjectsToSync(local_record)
- yield self.purgeDeletedObjectsInBatches(local_record, deleted)
</del><ins>+ changed, removed = yield self.findObjectsToSync(local_record)
+ self.accounting(" Calendar objects changed={}, removed={}.".format(len(changed), len(removed)))
+ yield self.purgeDeletedObjectsInBatches(local_record, removed)
</ins><span class="cx"> yield self.updateChangedObjectsInBatches(local_record, changed)
</span><span class="cx">
</span><span class="cx"> yield self.updateSyncState(local_record, remote_token)
</span><ins>+ self.accounting("Completed: syncCalendar.")
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inTransactionWrapper
</span><span class="lines">@@ -500,6 +547,7 @@
</span><span class="cx"> @param migrationRecord: current migration record
</span><span class="cx"> @type localID: L{CalendarMigrationRecord}
</span><span class="cx"> """
</span><ins>+
</ins><span class="cx"> # Remote changes
</span><span class="cx"> remote_home = yield self._remoteHome(txn)
</span><span class="cx"> remote_calendar = yield remote_home.childWithID(migrationRecord.remoteResourceID)
</span><span class="lines">@@ -510,6 +558,7 @@
</span><span class="cx"> local_home = yield self._localHome(txn)
</span><span class="cx"> local_calendar = yield local_home.childWithID(migrationRecord.localResourceID)
</span><span class="cx"> yield local_calendar.copyMetadata(remote_calendar)
</span><ins>+ self.accounting(" Copied calendar meta-data for calendar local-id={0.localResourceID}, remote-id={0.remoteResourceID}.".format(migrationRecord))
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inTransactionWrapper
</span><span class="lines">@@ -592,6 +641,7 @@
</span><span class="cx">
</span><span class="cx"> for local_object in local_objects:
</span><span class="cx"> yield local_object.purge()
</span><ins>+ self.accounting(" Purged calendar object local-id={}.".format(local_object.id()))
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -660,6 +710,7 @@
</span><span class="cx"> local_object = yield local_objects[obj_name]
</span><span class="cx"> yield local_object._setComponentInternal(remote_data, internal_state=ComponentUpdateState.RAW)
</span><span class="cx"> del local_objects[obj_name]
</span><ins>+ log_op = "Updated"
</ins><span class="cx"> else:
</span><span class="cx"> local_object = yield local_calendar._createCalendarObjectWithNameInternal(obj_name, remote_data, internal_state=ComponentUpdateState.RAW)
</span><span class="cx">
</span><span class="lines">@@ -671,13 +722,16 @@
</span><span class="cx"> remoteResourceID=remote_object.id(),
</span><span class="cx"> localResourceID=local_object.id()
</span><span class="cx"> )
</span><ins>+ log_op = "Created"
</ins><span class="cx">
</span><span class="cx"> # Sync meta-data such as schedule object, schedule tags, access mode etc
</span><span class="cx"> yield local_object.copyMetadata(remote_object)
</span><ins>+ self.accounting(" {} calendar object local-id={}, remote-id={}.".format(log_op, local_object.id(), remote_object.id()))
</ins><span class="cx">
</span><span class="cx"> # Purge the ones that remain
</span><span class="cx"> for local_object in local_objects.values():
</span><span class="cx"> yield local_object.purge()
</span><ins>+ self.accounting(" Purged calendar object local-id={}.".format(local_object.id()))
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -686,12 +740,17 @@
</span><span class="cx"> Sync attachments (both metadata and actual attachment data) for the home being migrated.
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ self.accounting("Starting: syncAttachments...")
+
</ins><span class="cx"> # Two steps - sync the table first in one txn, then sync each attachment's data
</span><span class="cx"> changed_ids, removed_ids = yield self.syncAttachmentTable()
</span><ins>+ self.accounting(" Attachments changed={}, removed={}".format(len(changed_ids), len(removed_ids)))
</ins><span class="cx">
</span><span class="cx"> for local_id in changed_ids:
</span><span class="cx"> yield self.syncAttachmentData(local_id)
</span><span class="cx">
</span><ins>+ self.accounting("Completed: syncAttachments.")
+
</ins><span class="cx"> returnValue((changed_ids, removed_ids,))
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -772,6 +831,7 @@
</span><span class="cx"> if records:
</span><span class="cx"> # Read the data from the conduit
</span><span class="cx"> yield remote_home.readAttachmentData(records[0].remoteResourceID, attachment)
</span><ins>+ self.accounting(" Read attachment local-id={0.localResourceID}, remote-id={0.remoteResourceID}".format(records[0]))
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -780,8 +840,11 @@
</span><span class="cx"> Link attachments to the calendar objects they belong to.
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ self.accounting("Starting: linkAttachments...")
+
</ins><span class="cx"> # Get the map of links for the remote home
</span><span class="cx"> links = yield self.getAttachmentLinks()
</span><ins>+ self.accounting(" Linking {} attachments".format(len(links)))
</ins><span class="cx">
</span><span class="cx"> # Get remote->local ID mappings
</span><span class="cx"> attachmentIDMap, objectIDMap = yield self.getAttachmentMappings()
</span><span class="lines">@@ -792,6 +855,8 @@
</span><span class="cx"> yield self.makeAttachmentLinks(links[:50], attachmentIDMap, objectIDMap)
</span><span class="cx"> links = links[50:]
</span><span class="cx">
</span><ins>+ self.accounting("Completed: linkAttachments.")
+
</ins><span class="cx"> returnValue(len_links)
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -854,11 +919,15 @@
</span><span class="cx"> a fake directory UID locally.
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ self.accounting("Starting: delegateReconcile...")
+
</ins><span class="cx"> yield self.individualDelegateReconcile()
</span><span class="cx"> yield self.groupDelegateReconcile()
</span><span class="cx"> yield self.externalDelegateReconcile()
</span><span class="cx">
</span><ins>+ self.accounting("Completed: delegateReconcile.")
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inTransactionWrapper
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def individualDelegateReconcile(self, txn):
</span><span class="lines">@@ -870,7 +939,9 @@
</span><span class="cx"> for record in remote_records:
</span><span class="cx"> yield record.insert(txn)
</span><span class="cx">
</span><ins>+ self.accounting(" Found {} individual delegates".format(len(remote_records)))
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inTransactionWrapper
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def groupDelegateReconcile(self, txn):
</span><span class="lines">@@ -885,7 +956,9 @@
</span><span class="cx"> delegator.groupID = local_group.groupID
</span><span class="cx"> yield delegator.insert(txn)
</span><span class="cx">
</span><ins>+ self.accounting(" Found {} group delegates".format(len(remote_records)))
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inTransactionWrapper
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def externalDelegateReconcile(self, txn):
</span><span class="lines">@@ -897,15 +970,20 @@
</span><span class="cx"> for record in remote_records:
</span><span class="cx"> yield record.insert(txn)
</span><span class="cx">
</span><ins>+ self.accounting(" Found {} external delegates".format(len(remote_records)))
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def groupAttendeeReconcile(self):
</span><span class="cx"> """
</span><span class="cx"> Sync the remote group attendee links to the local store.
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ self.accounting("Starting: groupAttendeeReconcile...")
+
</ins><span class="cx"> # Get remote data and local mapping information
</span><span class="cx"> remote_group_attendees, objectIDMap = yield self.groupAttendeeData()
</span><ins>+ self.accounting(" Found {} group attendees".format(len(remote_group_attendees)))
</ins><span class="cx">
</span><span class="cx"> # Map each result to a local resource (in batches)
</span><span class="cx"> number_of_links = len(remote_group_attendees)
</span><span class="lines">@@ -913,6 +991,8 @@
</span><span class="cx"> yield self.groupAttendeeProcess(remote_group_attendees[:50], objectIDMap)
</span><span class="cx"> remote_group_attendees = remote_group_attendees[50:]
</span><span class="cx">
</span><ins>+ self.accounting("Completed: groupAttendeeReconcile.")
+
</ins><span class="cx"> returnValue(number_of_links)
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -957,7 +1037,9 @@
</span><span class="cx"> Sync all the existing L{NotificationObject} resources from the remote store.
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ self.accounting("Starting: notificationsReconcile...")
</ins><span class="cx"> records = yield self.notificationRecords()
</span><ins>+ self.accounting(" Found {} notifications".format(len(records)))
</ins><span class="cx">
</span><span class="cx"> # Batch setting resources for the local home
</span><span class="cx"> len_records = len(records)
</span><span class="lines">@@ -965,6 +1047,8 @@
</span><span class="cx"> yield self.makeNotifications(records[:50])
</span><span class="cx"> records = records[50:]
</span><span class="cx">
</span><ins>+ self.accounting("Completed: notificationsReconcile.")
+
</ins><span class="cx"> returnValue(len_records)
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -998,7 +1082,8 @@
</span><span class="cx"> for record in records:
</span><span class="cx"> # Do this via the "write" API so that sync revisions are updated properly, rather than just
</span><span class="cx"> # inserting the records directly.
</span><del>- yield notifications.writeNotificationObject(record.notificationUID, record.notificationType, record.notificationData)
</del><ins>+ notification = yield notifications.writeNotificationObject(record.notificationUID, record.notificationType, record.notificationData)
+ self.accounting(" Added notification local-id={}.".format(notification.id()))
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -1019,6 +1104,7 @@
</span><span class="cx"> A -> C | B -> C (new) | (removed)
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ self.accounting("Starting: sharedByCollectionsReconcile...")
</ins><span class="cx"> calendars = yield self.getSyncState()
</span><span class="cx">
</span><span class="cx"> len_records = 0
</span><span class="lines">@@ -1028,6 +1114,10 @@
</span><span class="cx"> continue
</span><span class="cx"> records = records.items()
</span><span class="cx">
</span><ins>+ self.accounting(" Found shared by calendar local-id={0.localResourceID}, remote-id={0.remoteResourceID} with {1} sharees".format(
+ calendar, len(records),
+ ))
+
</ins><span class="cx"> # Batch setting resources for the local home
</span><span class="cx"> len_records += len(records)
</span><span class="cx"> while records:
</span><span class="lines">@@ -1040,6 +1130,8 @@
</span><span class="cx"> # Update the remote pod to switch over the shares
</span><span class="cx"> yield self.updatedRemoteSharedByCollections(calendar.remoteResourceID, bindUID)
</span><span class="cx">
</span><ins>+ self.accounting("Completed: sharedByCollectionsReconcile.")
+
</ins><span class="cx"> returnValue(len_records)
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -1093,12 +1185,14 @@
</span><span class="cx"> calendarResourceID=calendar_id,
</span><span class="cx"> bindRevision=0,
</span><span class="cx"> )
</span><ins>+ self.accounting(" Updating existing sharee {}".format(shareeHome.uid()))
</ins><span class="cx"> else:
</span><span class="cx"> # Map the record resource ids and insert a new record
</span><span class="cx"> record.calendarHomeResourceID = shareeHome.id()
</span><span class="cx"> record.calendarResourceID = calendar_id
</span><span class="cx"> record.bindRevision = 0
</span><span class="cx"> yield record.insert(txn)
</span><ins>+ self.accounting(" Adding new sharee {}".format(shareeHome.uid()))
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inTransactionWrapper
</span><span class="lines">@@ -1116,6 +1210,7 @@
</span><span class="cx"> share.groupID = local_group.groupID
</span><span class="cx"> share.calendarID = local_id
</span><span class="cx"> yield share.insert(txn)
</span><ins>+ self.accounting(" Adding group sharee {}".format(local_group.groupUID))
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inTransactionWrapper
</span><span class="lines">@@ -1128,6 +1223,7 @@
</span><span class="cx"> remote_home = yield self._remoteHome(txn)
</span><span class="cx"> remote_calendar = yield remote_home.childWithID(remote_id)
</span><span class="cx"> records = yield remote_calendar.migrateBindRecords(bindUID)
</span><ins>+ self.accounting(" Updating remote records")
</ins><span class="cx"> returnValue(records)
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -1147,14 +1243,20 @@
</span><span class="cx"> B -> A | B -> B (modify existing) | (removed)
</span><span class="cx"> C -> A | C -> B (new) | (removed)
</span><span class="cx"> """
</span><ins>+
+ self.accounting("Starting: sharedToCollectionsReconcile...")
+
</ins><span class="cx"> records = yield self.sharedToCollectionRecords()
</span><span class="cx"> records = records.items()
</span><span class="cx"> len_records = len(records)
</span><ins>+ self.accounting(" Found {} shared to collections".format(len_records))
</ins><span class="cx">
</span><span class="cx"> while records:
</span><span class="cx"> yield self.makeSharedToCollections(records[:50])
</span><span class="cx"> records = records[50:]
</span><span class="cx">
</span><ins>+ self.accounting("Completed: sharedToCollectionsReconcile.")
+
</ins><span class="cx"> returnValue(len_records)
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -1198,6 +1300,7 @@
</span><span class="cx"> yield oldrecord[0].update(
</span><span class="cx"> calendarHomeResourceID=self.homeId,
</span><span class="cx"> )
</span><ins>+ self.accounting(" Updated existing local sharer record {}".format(sharerHome.uid()))
</ins><span class="cx"> else:
</span><span class="cx"> raise AssertionError("An existing share must be present")
</span><span class="cx"> else:
</span><span class="lines">@@ -1217,6 +1320,7 @@
</span><span class="cx"> if oldrecord:
</span><span class="cx"> # Map the record resource ids and insert a new record
</span><span class="cx"> calendar_id = oldrecord.calendarResourceID
</span><ins>+ log_op = "Updated"
</ins><span class="cx"> else:
</span><span class="cx"> sharerView = yield sharerHome.createCollectionForExternalShare(
</span><span class="cx"> ownerRecord.calendarResourceName,
</span><span class="lines">@@ -1224,11 +1328,13 @@
</span><span class="cx"> metadataRecord.supportedComponents,
</span><span class="cx"> )
</span><span class="cx"> calendar_id = sharerView.id()
</span><ins>+ log_op = "Created"
</ins><span class="cx">
</span><span class="cx"> shareeRecord.calendarHomeResourceID = self.homeId
</span><span class="cx"> shareeRecord.calendarResourceID = calendar_id
</span><span class="cx"> shareeRecord.bindRevision = 0
</span><span class="cx"> yield shareeRecord.insert(txn)
</span><ins>+ self.accounting(" {} remote sharer record {}".format(log_op, sharerHome.uid()))
</ins><span class="cx">
</span><span class="cx"> yield self.updatedRemoteSharedToCollection(remote_id, txn=txn)
</span><span class="cx">
</span><span class="lines">@@ -1243,4 +1349,5 @@
</span><span class="cx"> remote_home = yield self._remoteHome(txn)
</span><span class="cx"> remote_calendar = yield remote_home.childWithID(remote_id)
</span><span class="cx"> records = yield remote_calendar.migrateBindRecords(None)
</span><ins>+ self.accounting(" Updating remote records")
</ins><span class="cx"> returnValue(records)
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationtesttest_home_syncpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py (14518 => 14519)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py        2015-03-06 19:34:30 UTC (rev 14518)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py        2015-03-06 20:13:58 UTC (rev 14519)
</span><span class="lines">@@ -32,7 +32,8 @@
</span><span class="cx"> ExternalDelegateGroupsRecord, DelegateGroupsRecord, GroupsRecord
</span><span class="cx"> from txdav.common.datastore.sql_notification import NotificationCollection
</span><span class="cx"> from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_EXTERNAL, \
</span><del>- _BIND_MODE_READ, _HOME_STATUS_MIGRATING
</del><ins>+ _BIND_MODE_READ, _HOME_STATUS_MIGRATING, _HOME_STATUS_NORMAL, \
+ _HOME_STATUS_DISABLED
</ins><span class="cx"> from txdav.common.datastore.test.util import populateCalendarsFrom
</span><span class="cx"> from txdav.who.delegates import Delegates
</span><span class="cx"> from txweb2.http_headers import MimeType
</span><span class="lines">@@ -981,7 +982,35 @@
</span><span class="cx"> yield self.commitTransaction(1)
</span><span class="cx">
</span><span class="cx">
</span><ins>+ @inlineCallbacks
+ def test_disable_remote_home(self):
+ """
+ Test that L{disableRemoteHome} changes the remote status and prevents a normal state
+ home from being created.
+ """
</ins><span class="cx">
</span><ins>+ # Create remote home - and add some fake notifications
+ yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+ yield self.theTransactionUnderTest(0).notificationsWithUID("user01", create=True)
+ yield self.commitTransaction(0)
+
+ # Sync from remote side
+ syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01")
+ yield syncer.loadRecord()
+ yield syncer.prepareCalendarHome()
+ yield syncer.disableRemoteHome()
+
+ # It is disabled
+ home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01")
+ self.assertTrue(home is None)
+ home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", status=_HOME_STATUS_NORMAL)
+ self.assertTrue(home is None)
+ home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", status=_HOME_STATUS_DISABLED)
+ self.assertTrue(home is not None)
+ yield self.commitTransaction(0)
+
+
+
</ins><span class="cx"> class TestSharingSync(MultiStoreConduitTest):
</span><span class="cx"> """
</span><span class="cx"> Test that L{CrossPodHomeSync} sharing sync works.
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationtesttest_migrationpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_migration.py (14518 => 14519)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_migration.py        2015-03-06 19:34:30 UTC (rev 14518)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_migration.py        2015-03-06 20:13:58 UTC (rev 14519)
</span><span class="lines">@@ -72,6 +72,8 @@
</span><span class="cx"> config.GroupAttendees.Enabled = True
</span><span class="cx"> config.GroupAttendees.ReconciliationDelaySeconds = 0
</span><span class="cx"> config.GroupAttendees.AutoUpdateSecondsFromNow = 0
</span><ins>+ config.AccountingCategories.migration = True
+ config.AccountingPrincipals = ["*"]
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span></span></pre>
</div>
</div>
</body>
</html>