[CalendarServer-changes] [14339] CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav
source_changes at macosforge.org
source_changes at macosforge.org
Fri Jan 23 16:51:34 PST 2015
Revision: 14339
http://trac.calendarserver.org//changeset/14339
Author: cdaboo at apple.com
Date: 2015-01-23 16:51:33 -0800 (Fri, 23 Jan 2015)
Log Message:
-----------
Checkpoint current migration code.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/icalendarstore.py
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql.py
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_external.py
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_tables.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/__init__.py
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/__init__.py
CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py
Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py 2015-01-24 00:48:46 UTC (rev 14338)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -526,6 +526,48 @@
@inlineCallbacks
+ def copyMetadata(self, other):
+ """
+ Copy metadata from one L{CalendarObjectResource} to another. This is only
+ used during a migration step.
+ """
+ assert self._txn._migrating
+
+ # Simple attributes that can be copied over as-is
+ chm = schema.CALENDAR_HOME_METADATA
+ values = {
+ chm.ALARM_VEVENT_TIMED : other._alarm_vevent_timed,
+ chm.ALARM_VEVENT_ALLDAY : other._alarm_vevent_allday,
+ chm.ALARM_VTODO_TIMED : other._alarm_vtodo_timed,
+ chm.ALARM_VTODO_ALLDAY : other._alarm_vtodo_allday,
+ chm.AVAILABILITY : other._availability,
+ }
+
+ # Need to map the default collection references from the remote ids to
+ # the local ones using names
+ remote_calendars = yield other.loadChildren()
+ remote_calendars = dict([(calendar.id(), calendar,) for calendar in remote_calendars])
+ local_calendars = yield self.loadChildren()
+ local_calendars = dict([(calendar.name(), calendar,) for calendar in local_calendars])
+
+ for componentType in self._componentDefaultColumn.keys():
+ attr_name = self._componentDefaultAttribute[componentType]
+ remote_id = getattr(other, attr_name)
+ if remote_id is not None:
+ remote_calendar = remote_calendars.get(remote_id)
+ if remote_calendar is not None:
+ remote_id = local_calendars.get(remote_calendar.name())
+ setattr(self, attr_name, remote_id)
+ values[self._componentDefaultColumn[componentType]] = remote_id
+
+ # Update the local data
+ yield Update(
+ values,
+ Where=chm.RESOURCE_ID == self._resourceID
+ ).on(self._txn)
+
+
+ @inlineCallbacks
def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, mode):
"""
Determine if this calendar home contains any calendar objects which
@@ -3694,6 +3736,31 @@
@inlineCallbacks
+ def copyMetadata(self, other):
+ """
+ Copy metadata from one L{CalendarObjectResource} to another. This is only
+ used during a migration step.
+ """
+ assert self._txn._migrating
+
+ co = schema.CALENDAR_OBJECT
+ values = {
+ co.ATTACHMENTS_MODE : other._attachment,
+ co.DROPBOX_ID : other._dropboxID,
+ co.ACCESS : other._access,
+ co.SCHEDULE_OBJECT : other._schedule_object,
+ co.SCHEDULE_TAG : other._schedule_tag,
+ co.SCHEDULE_ETAGS : other._schedule_etags,
+ co.PRIVATE_COMMENTS : other._private_comments,
+ }
+
+ yield Update(
+ values,
+ Where=co.RESOURCE_ID == self._resourceID
+ ).on(self._txn)
+
+
+ @inlineCallbacks
def component(self, doUpdate=False):
"""
Read calendar data and validate/fix it. Do not raise a store error here
@@ -3859,6 +3926,15 @@
)
+ def purge(self):
+ """
+ Do a "silent" removal of this object resource.
+ """
+ return self._removeInternal(
+ ComponentRemoveState.NORMAL_NO_IMPLICIT
+ )
+
+
@inlineCallbacks
def _removeInternal(self, internal_state=ComponentRemoveState.NORMAL):
Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/icalendarstore.py 2015-01-24 00:48:46 UTC (rev 14338)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/icalendarstore.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -906,7 +906,7 @@
is done (more than RAW).
RAW - store the supplied data as-is without any processing or validation. This is used
- for unit testing purposes only.
+ for unit testing purposes only, or during migration.
"""
NORMAL = NamedConstant()
Added: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/__init__.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -0,0 +1,15 @@
+##
+# 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.
+##
Added: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -0,0 +1,540 @@
+##
+# 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 twext.python.log import Logger
+
+from twisted.python.failure import Failure
+from twisted.internet.defer import returnValue, inlineCallbacks, succeed
+from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
+
+from functools import wraps
+from txdav.caldav.icalendarstore import ComponentUpdateState
+
+log = Logger()
+
+
+def inTransactionWrapper(operation):
+ """
+ This wrapper converts an instance method that takes a transaction as its
+ first parameter into one where the transaction parameter is an optional
+ keyword argument. If the keyword argument is present and not None, then
+ the instance method is called with that keyword as the first positional
+ argument (i.e., almost a NoOp). If the keyword argument is not present,
+ then a new transaction is created and the instance method called with
+ it as the first positional argument, plus the call is wrapped with
+ try/except/else to ensure proper commit and abort of the internally
+ created transaction is done.
+
+ So this wrapper allows for a method that requires a transaction to be run
+ with either an existing transaction or one created just for the purpose
+ of running it.
+
+ @param operation: a callable that takes an L{IAsyncTransaction} as its first
+ argument, and returns a value.
+ """
+
+ @wraps(operation)
+ @inlineCallbacks
+ def _inTxn(self, *args, **kwargs):
+ label = self.label(operation.__name__)
+ if "txn" in kwargs:
+ txn = kwargs["txn"]
+ del kwargs["txn"]
+ result = yield operation(self, txn, *args, **kwargs)
+ returnValue(result)
+ else:
+ txn = self.store.newTransaction(label=label)
+ try:
+ result = yield operation(self, txn, *args, **kwargs)
+ except Exception as ex:
+ f = Failure()
+ yield txn.abort()
+ log.error("{label} failed: {e}".format(label=label, e=str(ex)))
+ returnValue(f)
+ else:
+ yield txn.commit()
+ returnValue(result)
+
+ return _inTxn
+
+
+
+# Cross-pod synchronization of an entire calendar home
+class CrossPodHomeSync(object):
+
+ BATCH_SIZE = 50
+
+ def __init__(self, store, diruid):
+ """
+ @param store: the data store
+ @type store: L{CommonDataStore}
+ @param diruid: directory uid of the user whose home is to be sync'd
+ @type diruid: L{str}
+ """
+
+ self.store = store
+ self.diruid = diruid
+
+
+ def label(self, detail):
+ return "Cross-pod Migration Sync for {}: {}".format(self.diruid, detail)
+
+
+ def migratingUid(self):
+ return "Migrating-{}".format(self.diruid)
+
+
+ @inlineCallbacks
+ def migrateHere(self):
+ """
+ This is a full, serialized version of a data migration (minus any directory
+ update) that can be triggered via a command line tool. It is designed to
+ minimize down time for the migrating user.
+ """
+
+ # Step 1 - initial full sync
+ yield self.sync()
+
+ # Step 2 - increment sync (since the initial sync may take a long time
+ # to run we should do one incremental sync before bringing down the
+ # account being migrated)
+ yield self.sync()
+
+ # Step 3 - disable remote home
+ # NB Any failure from this point on will need to be caught and
+ # handled by re-enabling the old home (and fixing any sharing state
+ # that may have been changed)
+ yield self.disableRemoteHome()
+
+ # Step 4 - final incremental sync
+ yield self.sync()
+
+ # Step 5 - final overell sync of meta-data (including sharing re-linking)
+ yield self.finalSync()
+
+ # Step 6 - enable new home
+ yield self.enableLocalHome()
+
+ # Step 7 - remote remote home
+ yield self.removeRemoteHome()
+
+ # Step 8 - say phew! TODO: Actually alert everyone else
+ pass
+
+
+ @inlineCallbacks
+ def sync(self):
+ """
+ Initiate a sync of the home. This is a simple data sync that does not
+ reconcile sharing state etc. The L{finalSync} method will do a full
+ sharing reconcile as well as disable the migration source home.
+ """
+
+ yield self.loadRecord()
+ self.homeId = yield self.prepareCalendarHome()
+
+ yield self.syncCalendarList()
+
+ # TODO: sync home metadata such as alarms, default calendars, etc
+ yield self.syncCalendarHomeMetaData()
+
+ # TODO: sync attachments
+ pass
+
+ # TODO: group attendee reconcile
+ pass
+
+
+ @inlineCallbacks
+ def finalSync(self):
+ """
+ Do the final sync up of any additional data, re-link sharing bind
+ rows, recalculate quota etc.
+ """
+
+ # TODO:
+ pass
+
+
+ @inlineCallbacks
+ def disableRemoteHome(self):
+ """
+ Mark the remote home as disabled.
+ """
+
+ # TODO: implement API on CommonHome to rename the ownerUID column and
+ # change the status column.
+ pass
+
+
+ @inlineCallbacks
+ def enableLocalHome(self):
+ """
+ Mark the local home as enabled.
+ """
+
+ # TODO: implement API on CommonHome to rename the ownerUID column and
+ # change the status column.
+ pass
+
+
+ @inlineCallbacks
+ def removeRemoteHome(self):
+ """
+ Remove all the old data on the remote pod.
+ """
+
+ # TODO: implement API on CommonHome to purge the old data without
+ # any side-effects (scheduling, sharing etc).
+ pass
+
+
+ @inlineCallbacks
+ def loadRecord(self):
+ """
+ Initiate a sync of the home.
+ """
+
+ self.record = yield self.store.directoryService().recordWithUID(self.diruid)
+ if self.record is None:
+ raise DirectoryRecordNotFoundError("Cross-pod Migration Sync missing directory record for {}".format(self.diruid))
+ if self.record.thisServer():
+ raise ValueError("Cross-pod Migration Sync cannot sync with user already on this server: {}".format(self.diruid))
+
+
+ @inTransactionWrapper
+ @inlineCallbacks
+ def prepareCalendarHome(self, txn):
+ """
+ Make sure the inactive home to migrate into is present on this pod.
+ """
+
+ home = yield txn.calendarHomeWithUID(self.migratingUid())
+ if home is None:
+ home = yield txn.calendarHomeWithUID(self.migratingUid(), create=True, migratingUID=self.diruid)
+ returnValue(home.id())
+
+
+ @inTransactionWrapper
+ @inlineCallbacks
+ def syncCalendarHomeMetaData(self, txn):
+ """
+ Make sure the home meta-data (alarms, default calendars) is properly sync'd
+ """
+
+ remote_home = yield self._remoteHome(txn=txn)
+ local_home = yield txn.calendarHomeWithUID(self.migratingUid())
+ yield local_home.copyMetadata(remote_home)
+
+
+ @inlineCallbacks
+ def _remoteHome(self, txn):
+ """
+ Create a synthetic external home object that maps to the actual remote home.
+
+ @param ownerUID: directory uid of the user's home
+ @type ownerUID: L{str}
+ """
+
+ from txdav.caldav.datastore.sql_external import CalendarHomeExternal
+ resourceID = yield txn.store().conduit.send_home_resource_id(self, self.record)
+ home = CalendarHomeExternal(txn, self.record.uid, resourceID) if resourceID is not None else None
+ if home:
+ home._childClass = home._childClass._externalClass
+ returnValue(home)
+
+
+ @inlineCallbacks
+ def syncCalendarList(self):
+ """
+ Synchronize each owned calendar.
+ """
+
+ # Remote sync details
+ remote_sync_state = yield self.getCalendarSyncList()
+
+ # TODO: get local sync details from local DB
+ local_sync_state = yield self.getSyncState()
+
+ # Remove local calendars no longer on the remote side
+ yield self.purgeLocal(local_sync_state, remote_sync_state)
+
+ # Sync each calendar that matches on both sides
+ for name in remote_sync_state.keys():
+ yield self.syncCalendar(name, local_sync_state, remote_sync_state)
+
+
+ @inTransactionWrapper
+ @inlineCallbacks
+ def getCalendarSyncList(self, txn):
+ """
+ Get the names and sync-tokens for each remote owned calendar.
+ """
+
+ # List of calendars from the remote side
+ home = yield self._remoteHome(txn)
+ if home is None:
+ returnValue(None)
+ calendars = yield home.loadChildren()
+ results = {}
+ for calendar in calendars:
+ if calendar.owned():
+ sync_token = yield calendar.syncToken()
+ results[calendar.name()] = sync_token
+
+ returnValue(results)
+
+
+ def getSyncState(self):
+ """
+ Get local synchronization state for the home being migrated.
+ """
+ return succeed({})
+
+
+ @inTransactionWrapper
+ def setSyncState(self, txn, details):
+ """
+ Get local synchronization state for the home being migrated.
+ """
+ return succeed(None)
+
+
+ @inTransactionWrapper
+ @inlineCallbacks
+ def purgeLocal(self, txn, local_sync_state, remote_sync_state):
+ """
+ Remove (silently - i.e., no scheduling) local calendars that are no longer on the remote side.
+
+ @param txn: transaction to use
+ @type txn: L{CommonStoreTransaction}
+ @param local_sync_state: local sync state
+ @type local_sync_state: L{dict}
+ @param remote_sync_state: remote sync state
+ @type remote_sync_state: L{dict}
+ """
+ home = yield txn.calendarHomeWithUID(self.migratingUid())
+ for name in set(local_sync_state.keys()) - set(remote_sync_state.keys()):
+ calendar = yield home.childWithName(name)
+ if calendar is not None:
+ yield calendar.purge()
+ del local_sync_state[name]
+
+ yield self.setSyncState(local_sync_state, txn=txn)
+
+
+ @inlineCallbacks
+ def syncCalendar(self, name, local_sync_state, remote_sync_state):
+ """
+ Sync the contents of a calendar from the remote side. The local calendar may need to be created
+ on initial sync. Make use of sync tokens to avoid unnecessary work.
+
+ @param name: name of the calendar to sync
+ @type name: L{str}
+ @param local_sync_state: local sync state
+ @type local_sync_state: L{dict}
+ @param remote_sync_state: remote sync state
+ @type remote_sync_state: L{dict}
+ """
+
+ local_token = local_sync_state.get(name, None)
+ remote_token = remote_sync_state[name]
+ if local_token != remote_token:
+ # See if we need to create the local one first
+ if local_token is None:
+ yield self.newCalendar(name)
+
+ # TODO: sync meta-data such as alarms, supported-components, transp, etc
+ pass
+
+ # Sync object resources
+ changed, deleted = yield self.findObjectsToSync(name, local_token)
+ yield self.purgeDeletedObjectsInBatches(name, deleted)
+ yield self.updateChangedObjectsInBatches(name, changed)
+
+ local_sync_state[name] = remote_token
+ yield self.setSyncState(local_sync_state)
+
+
+ @inTransactionWrapper
+ @inlineCallbacks
+ def newCalendar(self, txn, name):
+ """
+ Create a new local calendar to sync remote data to.
+
+ @param name: name of the calendar to create
+ @type name: L{str}
+ """
+
+ home = yield txn.calendarHomeWithUID(self.migratingUid())
+ calendar = yield home.childWithName(name)
+ if calendar is None:
+ yield home.createChildWithName(name)
+
+
+ @inTransactionWrapper
+ @inlineCallbacks
+ def findObjectsToSync(self, txn, name, local_token):
+ """
+ Find the set of object resources that need to be sync'd from the remote
+ side and the set that need to be removed locally. Take into account the
+ possibility that this is a partial sync and removals or additions might
+ be false positives.
+
+ @param name: name of the calendar to sync
+ @type name: L{str}
+ @param local_token: sync token last used to sync the calendar
+ @type local_token: L{str}
+ """
+
+ # Remote changes
+ remote_home = yield self._remoteHome(txn)
+ remote_calendar = yield remote_home.childWithName(name)
+ if remote_calendar is None:
+ returnValue(None)
+ changed, deleted, _ignore_invalid = yield remote_calendar.resourceNamesSinceToken(local_token)
+
+ # Check whether the deleted set items
+ local_home = yield txn.calendarHomeWithUID(self.migratingUid())
+ local_calendar = yield local_home.childWithName(name)
+
+ # Check the md5's on each changed remote with the local one to filter out ones
+ # we don't actually need to sync
+ remote_changes = yield remote_calendar.objectResourcesWithNames(changed)
+ remote_changes = dict([(calendar.name(), calendar) for calendar in remote_changes])
+
+ local_changes = yield local_calendar.objectResourcesWithNames(changed)
+ local_changes = dict([(calendar.name(), calendar) for calendar in local_changes])
+
+ actual_changes = []
+ for name, calendar in remote_changes.items():
+ if name not in local_changes or remote_changes[name].md5() != local_changes[name].md5():
+ actual_changes.append(name)
+
+ returnValue((actual_changes, deleted,))
+
+
+ @inlineCallbacks
+ def purgeDeletedObjectsInBatches(self, name, deleted):
+ """
+ Purge (silently remove) the specified object resources. This needs to
+ succeed in the case where some or all resources have already been deleted.
+ Do this in batches to keep transaction times small.
+
+ @param name: name of the calendar to purge from
+ @type name: L{str}
+ @param deleted: list of names to purge
+ @type deleted: L{list} of L{str}
+ """
+
+ remaining = list(deleted)
+ while remaining:
+ yield self.purgeBatch(name, remaining[:self.BATCH_SIZE])
+ del remaining[:self.BATCH_SIZE]
+
+
+ @inTransactionWrapper
+ @inlineCallbacks
+ def purgeBatch(self, txn, name, purge_names):
+ """
+ Purge a bunch of object resources from the specified calendar.
+
+ @param txn: transaction to use
+ @type txn: L{CommonStoreTransaction}
+ @param name: name of calendar
+ @type name: L{str}
+ @param purge_names: object resource names to purge
+ @type purge_names: L{list} of L{str}
+ """
+
+ # Check whether the deleted set items
+ local_home = yield txn.calendarHomeWithUID(self.migratingUid())
+ local_calendar = yield local_home.childWithName(name)
+ local_objects = yield local_calendar.objectResourcesWithNames(purge_names)
+
+ for local_object in local_objects:
+ yield local_object.purge()
+
+
+ @inlineCallbacks
+ def updateChangedObjectsInBatches(self, name, changed):
+ """
+ Update the specified object resources. This needs to succeed in the
+ case where some or all resources have already been deleted.
+ Do this in batches to keep transaction times small.
+
+ @param name: name of the calendar to purge from
+ @type name: L{str}
+ @param changed: list of names to update
+ @type changed: L{list} of L{str}
+ """
+
+ remaining = list(changed)
+ while remaining:
+ yield self.updateBatch(name, remaining[:self.BATCH_SIZE])
+ del remaining[:self.BATCH_SIZE]
+
+
+ @inTransactionWrapper
+ @inlineCallbacks
+ def updateBatch(self, txn, name, remaining):
+ """
+ Update a bunch of object resources from the specified remote calendar.
+
+ @param txn: transaction to use
+ @type txn: L{CommonStoreTransaction}
+ @param name: name of calendar
+ @type name: L{str}
+ @param purge_names: object resource names to update
+ @type purge_names: L{list} of L{str}
+ """
+
+ # Get remote objects
+ remote_home = yield self._remoteHome(txn)
+ remote_calendar = yield remote_home.childWithName(name)
+ if remote_calendar is None:
+ returnValue(None)
+ remote_objects = yield remote_calendar.objectResourcesWithNames(remaining)
+ remote_objects = dict([(obj.name(), obj) for obj in remote_objects])
+
+ # Get local objects
+ local_home = yield txn.calendarHomeWithUID(self.migratingUid())
+ local_calendar = yield local_home.childWithName(name)
+ local_objects = yield local_calendar.objectResourcesWithNames(remaining)
+ local_objects = dict([(obj.name(), obj) for obj in local_objects])
+
+ # Sync ones that still exist - use txn._migrating together with stuffing the remote md5
+ # value onto the component being stored to ensure that the md5 value stored locally
+ # matches the remote one (which should help reduce the need for a client to resync
+ # the data when moved from one pod to the other).
+ txn._migrating = True
+ for obj_name in remote_objects.keys():
+ remote_object = remote_objects[obj_name]
+ remote_data = yield remote_object.component()
+ remote_data.md5 = remote_object.md5()
+ if obj_name in local_objects:
+ local_object = yield local_objects[obj_name]
+ yield local_object._setComponentInternal(remote_data, internal_state=ComponentUpdateState.RAW)
+ del local_objects[obj_name]
+ else:
+ local_object = yield local_calendar._createCalendarObjectWithNameInternal(obj_name, remote_data, internal_state=ComponentUpdateState.RAW)
+
+ # Sync meta-data such as schedule object, schedule tags, access mode etc
+ yield local_object.copyMetadata(remote_object)
+
+ # Purge the ones that remain
+ for local_object in local_objects.values():
+ yield local_object.purge()
Added: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/__init__.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -0,0 +1,15 @@
+##
+# 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.
+##
Added: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -0,0 +1,300 @@
+##
+# Copyright (c) 2005-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 twisted.internet.defer import inlineCallbacks
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest
+from txdav.common.datastore.podding.migration.home_sync import CrossPodHomeSync
+from pycalendar.datetime import DateTime
+from twistedcaldav.ical import Component, normalize_iCalStr
+
+
+class TestConduitAPI(MultiStoreConduitTest):
+ """
+ Test that the conduit api works.
+ """
+
+ nowYear = {"now": DateTime.getToday().getYear()}
+
+ caldata1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ caldata1_changed = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T150000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance changed
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ caldata2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid2
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ caldata3 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid3
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ @inlineCallbacks
+ def test_remote_home(self):
+ """
+ Test that a remote home can be accessed.
+ """
+
+ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+ self.assertTrue(home01 is not None)
+ yield self.commitTransaction(0)
+
+ syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01")
+ yield syncer.loadRecord()
+ home = yield syncer._remoteHome(self.theTransactionUnderTest(1))
+ self.assertTrue(home is not None)
+ self.assertEqual(home.id(), home01.id())
+ yield self.commitTransaction(1)
+
+
+ @inlineCallbacks
+ def test_prepare_home(self):
+ """
+ Test that L{prepareCalendarHome} creates a home.
+ """
+
+ # No home present
+ syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01")
+ home = yield self.homeUnderTest(self.theTransactionUnderTest(1), name=syncer.migratingUid())
+ self.assertTrue(home is None)
+ yield self.commitTransaction(1)
+
+ yield syncer.prepareCalendarHome()
+
+ # Home is present
+ home = yield self.homeUnderTest(self.theTransactionUnderTest(1), name=syncer.migratingUid())
+ self.assertTrue(home is not None)
+ yield self.commitTransaction(1)
+
+
+ @inlineCallbacks
+ def test_prepare_home_external_txn(self):
+ """
+ Test that L{prepareCalendarHome} creates a home.
+ """
+
+ # No home present
+ syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01")
+ home = yield self.homeUnderTest(self.theTransactionUnderTest(1), name=syncer.migratingUid())
+ self.assertTrue(home is None)
+ yield self.commitTransaction(1)
+
+ yield syncer.prepareCalendarHome(txn=self.theTransactionUnderTest(1))
+ yield self.commitTransaction(1)
+
+ # Home is present
+ home = yield self.homeUnderTest(self.theTransactionUnderTest(1), name=syncer.migratingUid())
+ self.assertTrue(home is not None)
+ yield self.commitTransaction(1)
+
+
+ @inlineCallbacks
+ def test_get_calendar_sync_list(self):
+ """
+ Test that L{getCalendarSyncList} returns the correct results.
+ """
+
+ yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+ yield self.commitTransaction(0)
+ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01")
+ self.assertTrue(home01 is not None)
+ calendars01 = yield home01.loadChildren()
+ results01 = {}
+ for calendar in calendars01:
+ if calendar.owned():
+ sync_token = yield calendar.syncToken()
+ results01[calendar.name()] = sync_token
+ yield self.commitTransaction(0)
+
+ syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01")
+ yield syncer.loadRecord()
+ results = yield syncer.getCalendarSyncList()
+ self.assertEqual(results, results01)
+
+
+ @inlineCallbacks
+ def test_sync_calendar_initial_empty(self):
+ """
+ Test that L{syncCalendar} syncs an initially non-existent local calendar with
+ an empty remote calendar.
+ """
+
+ home0 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+ calendar0 = yield home0.childWithName("calendar")
+ remote_sync_token = yield calendar0.syncToken()
+ yield self.commitTransaction(0)
+
+ syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01")
+ yield syncer.loadRecord()
+ yield syncer.prepareCalendarHome()
+
+ # No local calendar exists yet
+ home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+ calendar1 = yield home1.childWithName("calendar")
+ self.assertTrue(calendar1 is None)
+ yield self.commitTransaction(1)
+
+ # Trigger sync of the one calendar
+ local_sync_state = {}
+ remote_sync_state = {"calendar": remote_sync_token}
+ yield syncer.syncCalendar(
+ "calendar",
+ local_sync_state,
+ remote_sync_state,
+ )
+ self.assertTrue("calendar" in local_sync_state)
+ self.assertEqual(local_sync_state["calendar"], remote_sync_state["calendar"])
+
+ # Local calendar exists
+ home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+ calendar1 = yield home1.childWithName("calendar")
+ self.assertTrue(calendar1 is not None)
+ yield self.commitTransaction(1)
+
+
+ @inlineCallbacks
+ def test_sync_calendar_initial_with_data(self):
+ """
+ Test that L{syncCalendar} syncs an initially non-existent local calendar with
+ a remote calendar containing data. Also check a change to one event is then
+ sync'd the second time.
+ """
+
+ home0 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+ calendar0 = yield home0.childWithName("calendar")
+ yield calendar0.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield calendar0.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+ yield calendar0.createCalendarObjectWithName("3.ics", Component.fromString(self.caldata3))
+ yield self.commitTransaction(0)
+
+ syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01")
+ yield syncer.loadRecord()
+ yield syncer.prepareCalendarHome()
+
+ # No local calendar exists yet
+ home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+ calendar1 = yield home1.childWithName("calendar")
+ self.assertTrue(calendar1 is None)
+ yield self.commitTransaction(1)
+
+ # Trigger sync of the one calendar
+ local_sync_state = {}
+ remote_sync_state = yield syncer.getCalendarSyncList()
+ yield syncer.syncCalendar(
+ "calendar",
+ local_sync_state,
+ remote_sync_state,
+ )
+ self.assertTrue("calendar" in local_sync_state)
+ self.assertEqual(local_sync_state["calendar"], remote_sync_state["calendar"])
+
+ # Local calendar exists
+ home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+ calendar1 = yield home1.childWithName("calendar")
+ self.assertTrue(calendar1 is not None)
+ children = yield calendar1.listObjectResources()
+ self.assertEqual(set(children), set(("1.ics", "2.ics", "3.ics",)))
+ yield self.commitTransaction(1)
+
+ # Change one resource
+ object0 = yield self.calendarObjectUnderTest(
+ txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics"
+ )
+ yield object0.setComponent(Component.fromString(self.caldata1_changed))
+ yield self.commitTransaction(0)
+
+ remote_sync_state = yield syncer.getCalendarSyncList()
+ yield syncer.syncCalendar(
+ "calendar",
+ local_sync_state,
+ remote_sync_state,
+ )
+
+ object1 = yield self.calendarObjectUnderTest(
+ txn=self.theTransactionUnderTest(1), home=syncer.migratingUid(), calendar_name="calendar", name="1.ics"
+ )
+ caldata = yield object1.component()
+ self.assertEqual(normalize_iCalStr(caldata), normalize_iCalStr(self.caldata1_changed))
+ yield self.commitTransaction(1)
+
+ # Remove one resource
+ object0 = yield self.calendarObjectUnderTest(
+ txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="2.ics"
+ )
+ yield object0.remove()
+ yield self.commitTransaction(0)
+
+ remote_sync_state = yield syncer.getCalendarSyncList()
+ yield syncer.syncCalendar(
+ "calendar",
+ local_sync_state,
+ remote_sync_state,
+ )
+
+ calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home=syncer.migratingUid(), name="calendar")
+ children = yield calendar1.listObjectResources()
+ self.assertEqual(set(children), set(("1.ics", "3.ics",)))
+ yield self.commitTransaction(1)
Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py 2015-01-24 00:48:46 UTC (rev 14338)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -350,7 +350,7 @@
StoreAPIConduitMixin._make_simple_action("homechild_objectwith", "objectWith", classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize)
StoreAPIConduitMixin._make_simple_action("homechild_movehere", "moveObjectResourceHere")
StoreAPIConduitMixin._make_simple_action("homechild_moveaway", "moveObjectResourceAway")
-StoreAPIConduitMixin._make_simple_action("homechild_synctoken", "syncToken")
+StoreAPIConduitMixin._make_simple_action("homechild_synctokenrevision", "syncTokenRevision")
StoreAPIConduitMixin._make_simple_action("homechild_resourcenamessincerevision", "resourceNamesSinceRevision", transform_send_result=StoreAPIConduitMixin._to_tuple)
StoreAPIConduitMixin._make_simple_action("homechild_search", "search")
Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql.py 2015-01-24 00:48:46 UTC (rev 14338)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -65,7 +65,7 @@
_BIND_MODE_INDIRECT, _BIND_MODE_OWN, _BIND_STATUS_ACCEPTED, \
_BIND_STATUS_DECLINED, _BIND_STATUS_DELETED, _BIND_STATUS_INVALID, \
_BIND_STATUS_INVITED, _HOME_STATUS_EXTERNAL, _HOME_STATUS_NORMAL, \
- _HOME_STATUS_PURGING, schema, splitSQLString
+ _HOME_STATUS_PURGING, schema, splitSQLString, _HOME_STATUS_MIGRATING
from txdav.common.icommondatastore import ConcurrentModification, \
RecordNotAllowedError, ExternalShareFailed, ShareNotAllowed, \
IndexedSearchException, NotFoundError
@@ -695,7 +695,7 @@
).on(self)
- def _determineMemo(self, storeType, uid, create=False, authzUID=None):
+ def _determineMemo(self, storeType, uid, create=False, authzUID=None, migratingUID=None):
"""
Determine the memo dictionary to use for homeWithUID.
"""
@@ -721,7 +721,7 @@
@memoizedKey("uid", _determineMemo)
- def homeWithUID(self, storeType, uid, create=False, authzUID=None):
+ def homeWithUID(self, storeType, uid, create=False, authzUID=None, migratingUID=None):
"""
We need to distinguish between various different users "looking" at a home and its
child resources because we have per-user properties that depend on which user is "looking".
@@ -733,15 +733,15 @@
if storeType not in (ECALENDARTYPE, EADDRESSBOOKTYPE):
raise RuntimeError("Unknown home type.")
- return self._homeClass[storeType].homeWithUID(self, uid, create, authzUID)
+ return self._homeClass[storeType].homeWithUID(self, uid, create, authzUID, migratingUID)
- def calendarHomeWithUID(self, uid, create=False, authzUID=None):
- return self.homeWithUID(ECALENDARTYPE, uid, create=create, authzUID=authzUID)
+ def calendarHomeWithUID(self, uid, create=False, authzUID=None, migratingUID=None):
+ return self.homeWithUID(ECALENDARTYPE, uid, create=create, authzUID=authzUID, migratingUID=migratingUID)
- def addressbookHomeWithUID(self, uid, create=False, authzUID=None):
- return self.homeWithUID(EADDRESSBOOKTYPE, uid, create=create, authzUID=authzUID)
+ def addressbookHomeWithUID(self, uid, create=False, authzUID=None, migratingUID=None):
+ return self.homeWithUID(EADDRESSBOOKTYPE, uid, create=create, authzUID=authzUID, migratingUID=migratingUID)
@inlineCallbacks
@@ -3231,7 +3231,7 @@
@classmethod
@inlineCallbacks
- def homeWithUID(cls, txn, uid, create=False, authzUID=None):
+ def homeWithUID(cls, txn, uid, create=False, authzUID=None, migratingUID=None):
"""
@param uid: I'm going to assume uid is utf-8 encoded bytes
"""
@@ -3243,11 +3243,17 @@
returnValue(None)
# Determine if the user is local or external
- record = yield txn.directoryService().recordWithUID(uid.decode("utf-8"))
+ diruid = uid if migratingUID is None else migratingUID
+ record = yield txn.directoryService().recordWithUID(diruid.decode("utf-8"))
if record is None:
- raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
+ raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(diruid))
- state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
+ if migratingUID is None:
+ state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
+ else:
+ if record.thisServer():
+ raise RecordNotAllowedError("Cannot migrate a user data for a user already hosted on this server")
+ state = _HOME_STATUS_MIGRATING
# Use savepoint so we can do a partial rollback if there is a race condition
# where this row has already been inserted
@@ -3282,7 +3288,8 @@
# mapping in _cacher when creating as we don't want that to appear
# until AFTER the commit
home = yield cls.makeClass(txn, uid, no_cache=True, authzUID=authzUID)
- yield home.createdHome()
+ if migratingUID is None:
+ yield home.createdHome()
returnValue(home)
@@ -3357,6 +3364,15 @@
return self._status == _HOME_STATUS_PURGING
+ def migrating(self):
+ """
+ Is this an external home.
+
+ @return: a string.
+ """
+ return self._status == _HOME_STATUS_MIGRATING
+
+
def purge(self):
"""
Mark this home as being purged.
@@ -3364,6 +3380,13 @@
return self.setStatus(_HOME_STATUS_PURGING)
+ def migrate(self):
+ """
+ Mark this home as being purged.
+ """
+ return self.setStatus(_HOME_STATUS_MIGRATING)
+
+
@inlineCallbacks
def setStatus(self, newStatus):
"""
@@ -3598,13 +3621,18 @@
taken to invalid the cached value properly.
"""
if self._syncTokenRevision is None:
- self._syncTokenRevision = (yield self._syncTokenQuery.on(
- self._txn, resourceID=self._resourceID))[0][0]
- if self._syncTokenRevision is None:
- self._syncTokenRevision = int((yield self._txn.calendarserverValue("MIN-VALID-REVISION")))
+ self._syncTokenRevision = yield self.syncTokenRevision()
returnValue("%s_%s" % (self._resourceID, self._syncTokenRevision))
+ @inlineCallbacks
+ def syncTokenRevision(self):
+ revision = (yield self._syncTokenQuery.on(self._txn, resourceID=self._resourceID))[0][0]
+ if revision is None:
+ revision = int((yield self._txn.calendarserverValue("MIN-VALID-REVISION")))
+ returnValue(revision)
+
+
@classproperty
def _changesQuery(cls):
bind = cls._bindSchema
@@ -4127,13 +4155,18 @@
@inlineCallbacks
def syncToken(self):
if self._syncTokenRevision is None:
- self._syncTokenRevision = (yield self._childSyncTokenQuery.on(
- self._txn, resourceID=self._resourceID))[0][0]
- if self._syncTokenRevision is None:
- self._syncTokenRevision = int((yield self._txn.calendarserverValue("MIN-VALID-REVISION")))
+ self._syncTokenRevision = yield self.syncTokenRevision()
returnValue(("%s_%s" % (self._resourceID, self._syncTokenRevision,)))
+ @inlineCallbacks
+ def syncTokenRevision(self):
+ revision = (yield self._childSyncTokenQuery.on(self._txn, resourceID=self._resourceID))[0][0]
+ if revision is None:
+ revision = int((yield self._txn.calendarserverValue("MIN-VALID-REVISION")))
+ returnValue(revision)
+
+
def objectResourcesSinceToken(self, token):
raise NotImplementedError()
@@ -7410,6 +7443,13 @@
self._cachedComponent = None
+ def purge(self):
+ """
+ Do a "silent" removal of this object resource.
+ """
+ return self.remove()
+
+
def removeNotifyCategory(self):
"""
Indicates what category to use when determining the priority of push
@@ -7775,15 +7815,18 @@
@inlineCallbacks
def syncToken(self):
if self._syncTokenRevision is None:
- self._syncTokenRevision = (
- yield self._syncTokenQuery.on(
- self._txn, resourceID=self._resourceID)
- )[0][0]
- if self._syncTokenRevision is None:
- self._syncTokenRevision = int((yield self._txn.calendarserverValue("MIN-VALID-REVISION")))
+ self._syncTokenRevision = yield self.syncTokenRevision()
returnValue("%s_%s" % (self._resourceID, self._syncTokenRevision))
+ @inlineCallbacks
+ def syncTokenRevision(self):
+ revision = (yield self._syncTokenQuery.on(self._txn, resourceID=self._resourceID))[0][0]
+ if revision is None:
+ revision = int((yield self._txn.calendarserverValue("MIN-VALID-REVISION")))
+ returnValue(revision)
+
+
def properties(self):
return self._propertyStore
Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_external.py 2015-01-24 00:48:46 UTC (rev 14338)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_external.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -310,15 +310,14 @@
@inlineCallbacks
- def syncToken(self):
+ def syncTokenRevision(self):
if self._syncTokenRevision is None:
try:
- token = yield self._txn.store().conduit.send_homechild_synctoken(self)
- self._syncTokenRevision = self.revisionFromToken(token)
+ revision = yield self._txn.store().conduit.send_homechild_synctokenrevision(self)
except NonExistentExternalShare:
yield self.fixNonExistentExternalShare()
raise ExternalShareFailed("External share does not exist")
- returnValue(("%s_%s" % (self._externalID, self._syncTokenRevision,)))
+ returnValue(revision)
@inlineCallbacks
Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql 2015-01-24 00:48:46 UTC (rev 14338)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql 2015-01-24 00:51:33 UTC (rev 14339)
@@ -85,6 +85,7 @@
insert into HOME_STATUS values (0, 'normal' );
insert into HOME_STATUS values (1, 'external');
insert into HOME_STATUS values (2, 'purging');
+insert into HOME_STATUS values (3, 'migrating');
--------------
Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_tables.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_tables.py 2015-01-24 00:48:46 UTC (rev 14338)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_tables.py 2015-01-24 00:51:33 UTC (rev 14339)
@@ -187,6 +187,7 @@
_HOME_STATUS_NORMAL = _homeStatus('normal')
_HOME_STATUS_EXTERNAL = _homeStatus('external')
_HOME_STATUS_PURGING = _homeStatus('purging')
+_HOME_STATUS_MIGRATING = _homeStatus('migrating')
_bindStatus = _schemaConstants(
schema.CALENDAR_BIND_STATUS.DESCRIPTION,
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150123/de884161/attachment-0001.html>
More information about the calendarserver-changes
mailing list