config handling: added versioning

This patch adds version numbers to the on-disk config and files. The
goals are:
1. refuse to use a config which was written by a SyncEvolution release
   in a format that is too recent to be handled correctly by the
   current release ("detect invalid downgrades")
2. refuse to modify a config in such a way that the previous release
   using that config will not be able to use it anymore ("prevent
   unintentional upgrade of config")

In the first case the user is told:
  SyncEvolution <version> is too old to read configuration '<config>',
  please upgrade SyncEvolution.

In the second case the user is told to migrate the configuration manually:
  Proceeding would modify config '<config>' such that the
  previous SyncEvolution release will not be able to use it.
  Stopping now. Please explicitly acknowledge this step by
  running the following command on the command line:
  syncevolution --migrate '<config>'

These are printed as [ERROR] messages on the command line and shown as
error codes 22004 resp. 22005 in front-ends which don't know about these
scenarios.

The first problem should be rare, so presenting a nicer error messages
in UIs is not essential. But the second problem will occur. The plan
(not implemented yet) is to automatically migrate in stable releases
without asking.
This commit is contained in:
Patrick Ohly 2011-01-10 15:56:53 +01:00
parent 543ec4b4d8
commit 5684718722
6 changed files with 769 additions and 111 deletions

View File

@ -21,6 +21,7 @@
#include <syncevo/Cmdline.h>
#include <syncevo/FilterConfigNode.h>
#include <syncevo/VolatileConfigNode.h>
#include <syncevo/IniConfigNode.h>
#include <syncevo/SyncSource.h>
#include <syncevo/SyncContext.h>
#include <syncevo/util.h>
@ -730,6 +731,7 @@ bool Cmdline::run() {
sources = &m_sources;
}
boost::shared_ptr<SyncContext> to(createSyncClient());
to->prepareConfigForWrite();
to->copy(*from, sources);
// Sources are active now according to the server default.
@ -1799,6 +1801,9 @@ class CmdlineTest : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(CmdlineTest);
CPPUNIT_TEST(testFramework);
CPPUNIT_TEST(testSetupScheduleWorld);
CPPUNIT_TEST(testFutureConfig);
CPPUNIT_TEST(testPeerConfigMigration);
CPPUNIT_TEST(testContextConfigMigration);
CPPUNIT_TEST(testSetupDefault);
CPPUNIT_TEST(testSetupRenamed);
CPPUNIT_TEST(testSetupFunambol);
@ -1820,95 +1825,8 @@ class CmdlineTest : public CppUnit::TestFixture {
public:
CmdlineTest() :
m_testDir("CmdlineTest"),
// properties sorted by the order in which they are defined
// in the sync and sync source property registry
m_scheduleWorldConfig("peers/scheduleworld/.internal.ini:# HashCode = 0\n"
"peers/scheduleworld/.internal.ini:# ConfigDate = \n"
"peers/scheduleworld/.internal.ini:# lastNonce = \n"
"peers/scheduleworld/.internal.ini:# deviceData = \n"
"peers/scheduleworld/config.ini:syncURL = http://sync.scheduleworld.com/funambol/ds\n"
"peers/scheduleworld/config.ini:username = your SyncML server account name\n"
"peers/scheduleworld/config.ini:password = your SyncML server password\n"
"config.ini:# logdir = \n"
"peers/scheduleworld/config.ini:# loglevel = 0\n"
"peers/scheduleworld/config.ini:# printChanges = 1\n"
"peers/scheduleworld/config.ini:# dumpData = 1\n"
"config.ini:# maxlogdirs = 10\n"
"peers/scheduleworld/config.ini:# autoSync = 0\n"
"peers/scheduleworld/config.ini:# autoSyncInterval = 30M\n"
"peers/scheduleworld/config.ini:# autoSyncDelay = 5M\n"
"peers/scheduleworld/config.ini:# preventSlowSync = 1\n"
"peers/scheduleworld/config.ini:# useProxy = 0\n"
"peers/scheduleworld/config.ini:# proxyHost = \n"
"peers/scheduleworld/config.ini:# proxyUsername = \n"
"peers/scheduleworld/config.ini:# proxyPassword = \n"
"peers/scheduleworld/config.ini:# clientAuthType = md5\n"
"peers/scheduleworld/config.ini:# RetryDuration = 5M\n"
"peers/scheduleworld/config.ini:# RetryInterval = 2M\n"
"peers/scheduleworld/config.ini:# remoteIdentifier = \n"
"peers/scheduleworld/config.ini:# PeerIsClient = 0\n"
"peers/scheduleworld/config.ini:# SyncMLVersion = \n"
"peers/scheduleworld/config.ini:# PeerName = \n"
"config.ini:deviceId = fixed-devid\n" /* this is not the default! */
"peers/scheduleworld/config.ini:# remoteDeviceId = \n"
"peers/scheduleworld/config.ini:# enableWBXML = 1\n"
"peers/scheduleworld/config.ini:# maxMsgSize = 150000\n"
"peers/scheduleworld/config.ini:# maxObjSize = 4000000\n"
"peers/scheduleworld/config.ini:# enableCompression = 0\n"
"peers/scheduleworld/config.ini:# SSLServerCertificates = \n"
"peers/scheduleworld/config.ini:# SSLVerifyServer = 1\n"
"peers/scheduleworld/config.ini:# SSLVerifyHost = 1\n"
"peers/scheduleworld/config.ini:WebURL = http://www.scheduleworld.com\n"
"peers/scheduleworld/config.ini:# IconURI = \n"
"peers/scheduleworld/config.ini:# ConsumerReady = 0\n"
"peers/scheduleworld/sources/addressbook/.internal.ini:# adminData = \n"
"peers/scheduleworld/sources/addressbook/.internal.ini:# synthesisID = 0\n"
"peers/scheduleworld/sources/addressbook/config.ini:sync = two-way\n"
"sources/addressbook/config.ini:type = addressbook:text/vcard\n"
"peers/scheduleworld/sources/addressbook/config.ini:type = addressbook:text/vcard\n"
"sources/addressbook/config.ini:# evolutionsource = \n"
"peers/scheduleworld/sources/addressbook/config.ini:uri = card3\n"
"sources/addressbook/config.ini:# evolutionuser = \n"
"sources/addressbook/config.ini:# evolutionpassword = \n"
"peers/scheduleworld/sources/calendar/.internal.ini:# adminData = \n"
"peers/scheduleworld/sources/calendar/.internal.ini:# synthesisID = 0\n"
"peers/scheduleworld/sources/calendar/config.ini:sync = two-way\n"
"sources/calendar/config.ini:type = calendar\n"
"peers/scheduleworld/sources/calendar/config.ini:type = calendar\n"
"sources/calendar/config.ini:# evolutionsource = \n"
"peers/scheduleworld/sources/calendar/config.ini:uri = cal2\n"
"sources/calendar/config.ini:# evolutionuser = \n"
"sources/calendar/config.ini:# evolutionpassword = \n"
"peers/scheduleworld/sources/memo/.internal.ini:# adminData = \n"
"peers/scheduleworld/sources/memo/.internal.ini:# synthesisID = 0\n"
"peers/scheduleworld/sources/memo/config.ini:sync = two-way\n"
"sources/memo/config.ini:type = memo\n"
"peers/scheduleworld/sources/memo/config.ini:type = memo\n"
"sources/memo/config.ini:# evolutionsource = \n"
"peers/scheduleworld/sources/memo/config.ini:uri = note\n"
"sources/memo/config.ini:# evolutionuser = \n"
"sources/memo/config.ini:# evolutionpassword = \n"
"peers/scheduleworld/sources/todo/.internal.ini:# adminData = \n"
"peers/scheduleworld/sources/todo/.internal.ini:# synthesisID = 0\n"
"peers/scheduleworld/sources/todo/config.ini:sync = two-way\n"
"sources/todo/config.ini:type = todo\n"
"peers/scheduleworld/sources/todo/config.ini:type = todo\n"
"sources/todo/config.ini:# evolutionsource = \n"
"peers/scheduleworld/sources/todo/config.ini:uri = task2\n"
"sources/todo/config.ini:# evolutionuser = \n"
"sources/todo/config.ini:# evolutionpassword = ")
m_testDir("CmdlineTest")
{
#ifdef ENABLE_LIBSOUP
// path to SSL certificates has to be set only for libsoup
boost::replace_first(m_scheduleWorldConfig,
"SSLServerCertificates = ",
"SSLServerCertificates = /etc/ssl/certs/ca-certificates.crt:/etc/pki/tls/certs/ca-bundle.crt:/usr/share/ssl/certs/ca-bundle.crt");
#endif
}
protected:
@ -2002,6 +1920,218 @@ protected:
}
}
void expectTooOld() {
bool caught = false;
try {
SyncConfig config("scheduleworld");
} catch (const StatusException &ex) {
caught = true;
if (ex.syncMLStatus() != STATUS_RELEASE_TOO_OLD) {
throw;
} else {
CPPUNIT_ASSERT_EQUAL(StringPrintf("SyncEvolution %s is too old to read configuration 'scheduleworld', please upgrade SyncEvolution.", VERSION),
string(ex.what()));
}
}
CPPUNIT_ASSERT(caught);
}
void testFutureConfig() {
ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
ScopedEnvChange home("HOME", m_testDir);
rm_r(m_testDir);
doSetupScheduleWorld(false);
// bump min/cur version to something not supported, then
// try to read => should fail
IniFileConfigNode root(m_testDir, "/syncevolution/.internal.ini", false);
IniFileConfigNode context(m_testDir + "/syncevolution/default", ".internal.ini", false);
IniFileConfigNode peer(m_testDir + "/syncevolution/default/peers/scheduleworld", ".internal.ini", false);
root.setProperty("rootMinVersion", StringPrintf("%d", CONFIG_ROOT_MIN_VERSION + 1));
root.setProperty("rootCurVersion", StringPrintf("%d", CONFIG_ROOT_CUR_VERSION + 1));
root.flush();
context.setProperty("contextMinVersion", StringPrintf("%d", CONFIG_CONTEXT_MIN_VERSION + 1));
context.setProperty("contextCurVersion", StringPrintf("%d", CONFIG_CONTEXT_CUR_VERSION + 1));
context.flush();
peer.setProperty("peerMinVersion", StringPrintf("%d", CONFIG_PEER_MIN_VERSION + 1));
peer.setProperty("peerCurVersion", StringPrintf("%d", CONFIG_PEER_CUR_VERSION + 1));
peer.flush();
expectTooOld();
root.setProperty("rootMinVersion", StringPrintf("%d", CONFIG_ROOT_MIN_VERSION));
root.flush();
expectTooOld();
context.setProperty("contextMinVersion", StringPrintf("%d", CONFIG_CONTEXT_MIN_VERSION));
context.flush();
expectTooOld();
// okay now
peer.setProperty("peerMinVersion", StringPrintf("%d", CONFIG_PEER_MIN_VERSION));
peer.flush();
SyncConfig config("scheduleworld");
}
void expectMigration(const std::string &config) {
bool caught = false;
try {
SyncConfig c(config);
c.prepareConfigForWrite();
} catch (const StatusException &ex) {
caught = true;
if (ex.syncMLStatus() != STATUS_MIGRATION_NEEDED) {
throw;
} else {
CPPUNIT_ASSERT_EQUAL(StringPrintf("Proceeding would modify config '%s' such that the "
"previous SyncEvolution release will not be able to use it. "
"Stopping now. Please explicitly acknowledge this step by "
"running the following command on the command line: "
"syncevolution --migrate '%s'",
config.c_str(),
config.c_str()),
string(ex.what()));
}
}
CPPUNIT_ASSERT(caught);
}
void testPeerConfigMigration() {
ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
ScopedEnvChange home("HOME", m_testDir);
rm_r(m_testDir);
doSetupScheduleWorld(false);
// decrease min/cur version to something no longer supported,
// then try to write => should migrate in release mode and fail otherwise
IniFileConfigNode peer(m_testDir + "/syncevolution/default/peers/scheduleworld", ".internal.ini", false);
peer.setProperty("peerMinVersion", StringPrintf("%d", CONFIG_PEER_CUR_VERSION - 1));
peer.setProperty("peerCurVersion", StringPrintf("%d", CONFIG_PEER_CUR_VERSION - 1));
peer.flush();
SyncContext::setStableRelease(false);
expectMigration("scheduleworld");
SyncContext::setStableRelease(true);
{
SyncConfig config("scheduleworld");
config.prepareConfigForWrite();
}
{
TestCmdline cmdline("--print-servers", NULL);
cmdline.doit();
CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
" scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
" scheduleworld.old = CmdlineTest/syncevolution/default/peers/scheduleworld.old\n",
cmdline.m_out.str());
}
// should be okay now
SyncContext::setStableRelease(false);
{
SyncConfig config("scheduleworld");
config.prepareConfigForWrite();
}
// do the same migration with command line
SyncContext::setStableRelease(false);
rm_r(m_testDir + "/syncevolution/default/peers/scheduleworld");
CPPUNIT_ASSERT_EQUAL(0, rename((m_testDir + "/syncevolution/default/peers/scheduleworld.old").c_str(),
(m_testDir + "/syncevolution/default/peers/scheduleworld").c_str()));
{
TestCmdline cmdline("--migrate", "scheduleworld", NULL);
cmdline.doit();
}
{
SyncConfig config("scheduleworld");
config.prepareConfigForWrite();
}
{
TestCmdline cmdline("--print-servers", NULL);
cmdline.doit();
CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
" scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
" scheduleworld.old = CmdlineTest/syncevolution/default/peers/scheduleworld.old\n",
cmdline.m_out.str());
}
}
void testContextConfigMigration() {
ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
ScopedEnvChange home("HOME", m_testDir);
rm_r(m_testDir);
doSetupScheduleWorld(false);
// decrease min/cur version to something no longer supported,
// then try to write => should migrate in release mode and fail otherwise
IniFileConfigNode context(m_testDir + "/syncevolution/default", ".internal.ini", false);
context.setProperty("contextMinVersion", StringPrintf("%d", CONFIG_CONTEXT_CUR_VERSION - 1));
context.setProperty("contextCurVersion", StringPrintf("%d", CONFIG_CONTEXT_CUR_VERSION - 1));
context.flush();
#if 1
// context migration not implemented and not needed yet
SyncContext::setStableRelease(true);
bool caught = false;
try {
SyncConfig config("scheduleworld");
config.prepareConfigForWrite();
} catch (const Exception &ex) {
caught = true;
CPPUNIT_ASSERT_EQUAL(string("migration of config '@default' failed"),
string(ex.what()));
}
CPPUNIT_ASSERT(caught);
#else
SyncContext::setStableRelease(false);
expectMigration("@default");
SyncContext::setStableRelease(true);
{
SyncConfig config("@default");
config.prepareConfigForWrite();
}
{
TestCmdline cmdline("--print-servers", NULL);
cmdline.doit();
CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
" scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
" scheduleworld.old = CmdlineTest/syncevolution/default/peers/scheduleworld.old\n",
cmdline.m_out.str());
}
// should be okay now
SyncContext::setStableRelease(false);
{
SyncConfig config("@default");
config.prepareConfigForWrite();
}
// do the same migration with command line
SyncContext::setStableRelease(false);
rm_r(m_testDir + "/syncevolution/default/peers/scheduleworld");
CPPUNIT_ASSERT_EQUAL(0, rename((m_testDir + "/syncevolution/default/peers/scheduleworld.old").c_str(),
(m_testDir + "/syncevolution/default/peers/scheduleworld").c_str()));
{
TestCmdline cmdline("--migrate", "@default", NULL);
cmdline.doit();
}
{
SyncConfig config("@default");
config.prepareConfigForWrite();
}
{
TestCmdline cmdline("--print-servers", NULL);
cmdline.doit();
CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
" scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
" scheduleworld.old = CmdlineTest/syncevolution/default/peers/scheduleworld.old\n",
cmdline.m_out.str());
}
#endif
}
void testSetupDefault() {
string root;
ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
@ -2023,6 +2153,7 @@ protected:
boost::replace_all(expected, "/scheduleworld/", "/some-other-server/");
CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
}
void testSetupRenamed() {
string root;
ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
@ -2690,13 +2821,17 @@ protected:
string res = scanFiles(root);
removeRandomUUID(res);
string expected =
"config.ini:# logdir = \n"
"config.ini:# maxlogdirs = 10\n"
"config.ini:deviceId = fixed-devid\n"
"sources/addressbook/config.ini:type = file:text/vcard:3.0\n"
"sources/addressbook/config.ini:evolutionsource = file://tmp/test\n"
"sources/addressbook/config.ini:# evolutionuser = \n"
"sources/addressbook/config.ini:# evolutionpassword = \n";
StringPrintf(".internal.ini:contextMinVersion = %d\n"
".internal.ini:contextCurVersion = %d\n"
"config.ini:# logdir = \n"
"config.ini:# maxlogdirs = 10\n"
"config.ini:deviceId = fixed-devid\n"
"sources/addressbook/config.ini:type = file:text/vcard:3.0\n"
"sources/addressbook/config.ini:evolutionsource = file://tmp/test\n"
"sources/addressbook/config.ini:# evolutionuser = \n"
"sources/addressbook/config.ini:# evolutionpassword = \n",
CONFIG_CONTEXT_MIN_VERSION,
CONFIG_CONTEXT_CUR_VERSION);
CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
// add calendar
@ -2763,6 +2898,12 @@ protected:
"deviceData" +
"adminData" +
"synthesisID" +
"rootMinVersion" +
"rootCurVersion" +
"contextMinVersion" +
"contextCurVersion" +
"peerMinVersion" +
"peerCurVersion" +
"lastNonce" +
"last";
BOOST_FOREACH(string &prop, props) {
@ -2933,7 +3074,7 @@ protected:
CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
string migratedConfig = scanFiles(newRoot);
string expected = m_scheduleWorldConfig;
string expected = ScheduleWorldConfig();
sortConfig(expected);
boost::replace_first(expected,
"peers/scheduleworld/sources/addressbook/config.ini",
@ -2991,9 +3132,7 @@ protected:
}
}
const string m_testDir;
string m_scheduleWorldConfig;
const string m_testDir;
private:
@ -3053,8 +3192,104 @@ private:
boost::scoped_array<const char *> m_argv;
};
string ScheduleWorldConfig() {
string config = m_scheduleWorldConfig;
string ScheduleWorldConfig(int contextMinVersion = CONFIG_CONTEXT_MIN_VERSION,
int contextCurVersion = CONFIG_CONTEXT_CUR_VERSION,
int peerMinVersion = CONFIG_PEER_MIN_VERSION,
int peerCurVersion = CONFIG_PEER_CUR_VERSION) {
// properties sorted by the order in which they are defined
// in the sync and sync source property registry
string config =
StringPrintf("peers/scheduleworld/.internal.ini:peerMinVersion = %d\n"
"peers/scheduleworld/.internal.ini:peerCurVersion = %d\n"
"peers/scheduleworld/.internal.ini:# HashCode = 0\n"
"peers/scheduleworld/.internal.ini:# ConfigDate = \n"
"peers/scheduleworld/.internal.ini:# lastNonce = \n"
"peers/scheduleworld/.internal.ini:# deviceData = \n"
"peers/scheduleworld/config.ini:syncURL = http://sync.scheduleworld.com/funambol/ds\n"
"peers/scheduleworld/config.ini:username = your SyncML server account name\n"
"peers/scheduleworld/config.ini:password = your SyncML server password\n"
".internal.ini:contextMinVersion = %d\n"
".internal.ini:contextCurVersion = %d\n"
"config.ini:# logdir = \n"
"peers/scheduleworld/config.ini:# loglevel = 0\n"
"peers/scheduleworld/config.ini:# printChanges = 1\n"
"peers/scheduleworld/config.ini:# dumpData = 1\n"
"config.ini:# maxlogdirs = 10\n"
"peers/scheduleworld/config.ini:# autoSync = 0\n"
"peers/scheduleworld/config.ini:# autoSyncInterval = 30M\n"
"peers/scheduleworld/config.ini:# autoSyncDelay = 5M\n"
"peers/scheduleworld/config.ini:# preventSlowSync = 1\n"
"peers/scheduleworld/config.ini:# useProxy = 0\n"
"peers/scheduleworld/config.ini:# proxyHost = \n"
"peers/scheduleworld/config.ini:# proxyUsername = \n"
"peers/scheduleworld/config.ini:# proxyPassword = \n"
"peers/scheduleworld/config.ini:# clientAuthType = md5\n"
"peers/scheduleworld/config.ini:# RetryDuration = 5M\n"
"peers/scheduleworld/config.ini:# RetryInterval = 2M\n"
"peers/scheduleworld/config.ini:# remoteIdentifier = \n"
"peers/scheduleworld/config.ini:# PeerIsClient = 0\n"
"peers/scheduleworld/config.ini:# SyncMLVersion = \n"
"peers/scheduleworld/config.ini:# PeerName = \n"
"config.ini:deviceId = fixed-devid\n" /* this is not the default! */
"peers/scheduleworld/config.ini:# remoteDeviceId = \n"
"peers/scheduleworld/config.ini:# enableWBXML = 1\n"
"peers/scheduleworld/config.ini:# maxMsgSize = 150000\n"
"peers/scheduleworld/config.ini:# maxObjSize = 4000000\n"
"peers/scheduleworld/config.ini:# enableCompression = 0\n"
"peers/scheduleworld/config.ini:# SSLServerCertificates = \n"
"peers/scheduleworld/config.ini:# SSLVerifyServer = 1\n"
"peers/scheduleworld/config.ini:# SSLVerifyHost = 1\n"
"peers/scheduleworld/config.ini:WebURL = http://www.scheduleworld.com\n"
"peers/scheduleworld/config.ini:# IconURI = \n"
"peers/scheduleworld/config.ini:# ConsumerReady = 0\n"
"peers/scheduleworld/sources/addressbook/.internal.ini:# adminData = \n"
"peers/scheduleworld/sources/addressbook/.internal.ini:# synthesisID = 0\n"
"peers/scheduleworld/sources/addressbook/config.ini:sync = two-way\n"
"sources/addressbook/config.ini:type = addressbook:text/vcard\n"
"peers/scheduleworld/sources/addressbook/config.ini:type = addressbook:text/vcard\n"
"sources/addressbook/config.ini:# evolutionsource = \n"
"peers/scheduleworld/sources/addressbook/config.ini:uri = card3\n"
"sources/addressbook/config.ini:# evolutionuser = \n"
"sources/addressbook/config.ini:# evolutionpassword = \n"
"peers/scheduleworld/sources/calendar/.internal.ini:# adminData = \n"
"peers/scheduleworld/sources/calendar/.internal.ini:# synthesisID = 0\n"
"peers/scheduleworld/sources/calendar/config.ini:sync = two-way\n"
"sources/calendar/config.ini:type = calendar\n"
"peers/scheduleworld/sources/calendar/config.ini:type = calendar\n"
"sources/calendar/config.ini:# evolutionsource = \n"
"peers/scheduleworld/sources/calendar/config.ini:uri = cal2\n"
"sources/calendar/config.ini:# evolutionuser = \n"
"sources/calendar/config.ini:# evolutionpassword = \n"
"peers/scheduleworld/sources/memo/.internal.ini:# adminData = \n"
"peers/scheduleworld/sources/memo/.internal.ini:# synthesisID = 0\n"
"peers/scheduleworld/sources/memo/config.ini:sync = two-way\n"
"sources/memo/config.ini:type = memo\n"
"peers/scheduleworld/sources/memo/config.ini:type = memo\n"
"sources/memo/config.ini:# evolutionsource = \n"
"peers/scheduleworld/sources/memo/config.ini:uri = note\n"
"sources/memo/config.ini:# evolutionuser = \n"
"sources/memo/config.ini:# evolutionpassword = \n"
"peers/scheduleworld/sources/todo/.internal.ini:# adminData = \n"
"peers/scheduleworld/sources/todo/.internal.ini:# synthesisID = 0\n"
"peers/scheduleworld/sources/todo/config.ini:sync = two-way\n"
"sources/todo/config.ini:type = todo\n"
"peers/scheduleworld/sources/todo/config.ini:type = todo\n"
"sources/todo/config.ini:# evolutionsource = \n"
"peers/scheduleworld/sources/todo/config.ini:uri = task2\n"
"sources/todo/config.ini:# evolutionuser = \n"
"sources/todo/config.ini:# evolutionpassword = ",
peerMinVersion, peerCurVersion,
contextMinVersion, contextCurVersion);
#ifdef ENABLE_LIBSOUP
// path to SSL certificates has to be set only for libsoup
boost::replace_first(config,
"SSLServerCertificates = ",
"SSLServerCertificates = /etc/ssl/certs/ca-certificates.crt:/etc/pki/tls/certs/ca-bundle.crt:/usr/share/ssl/certs/ca-bundle.crt");
#endif
#if 0
// Currently we don't have an icon for ScheduleWorld. If we
@ -3147,7 +3382,7 @@ private:
}
string FunambolConfig() {
string config = m_scheduleWorldConfig;
string config = ScheduleWorldConfig();
boost::replace_all(config, "/scheduleworld/", "/funambol/");
boost::replace_first(config,
@ -3195,7 +3430,7 @@ private:
}
string SynthesisConfig() {
string config = m_scheduleWorldConfig;
string config = ScheduleWorldConfig();
boost::replace_all(config, "/scheduleworld/", "/synthesis/");
boost::replace_first(config,

View File

@ -29,6 +29,7 @@
#include <syncevo/DevNullConfigNode.h>
#include <syncevo/MultiplexConfigNode.h>
#include <syncevo/SingleFileConfigTree.h>
#include <syncevo/Cmdline.h>
#include <syncevo/lcs.h>
#include <test.h>
#include <synthesis/timeutil.h>
@ -51,6 +52,31 @@ static bool SourcePropSourceTypeIsSet(boost::shared_ptr<SyncSourceConfig> source
static bool SourcePropURIIsSet(boost::shared_ptr<SyncSourceConfig> source);
static bool SourcePropSyncIsSet(boost::shared_ptr<SyncSourceConfig> source);
int ConfigVersions[CONFIG_LEVEL_MAX][CONFIG_VERSION_MAX] =
{
{ CONFIG_ROOT_MIN_VERSION, CONFIG_ROOT_CUR_VERSION },
{ CONFIG_CONTEXT_MIN_VERSION, CONFIG_CONTEXT_CUR_VERSION },
{ CONFIG_PEER_MIN_VERSION, CONFIG_PEER_CUR_VERSION },
};
std::string ConfigLevel2String(ConfigLevel level)
{
switch (level) {
case CONFIG_LEVEL_ROOT:
return "config root";
break;
case CONFIG_LEVEL_CONTEXT:
return "context config";
break;
case CONFIG_LEVEL_PEER:
return "peer config";
break;
default:
return StringPrintf("config level %d (?)", level);
break;
}
}
void ConfigProperty::splitComment(const string &comment, list<string> &commentLines)
{
size_t start = 0;
@ -130,8 +156,16 @@ bool SyncConfig::splitConfigString(const string &config, string &peer, string &c
}
}
static SyncConfig::ConfigWriteMode defaultConfigWriteMode()
{
return SyncContext::isStableRelease() ?
SyncConfig::MIGRATE_AUTOMATICALLY :
SyncConfig::ASK_USER_TO_MIGRATE;
}
SyncConfig::SyncConfig() :
m_layout(HTTP_SERVER_LAYOUT) // use more compact layout with shorter paths and less source nodes
m_layout(HTTP_SERVER_LAYOUT), // use more compact layout with shorter paths and less source nodes
m_configWriteMode(defaultConfigWriteMode())
{
// initialize properties
SyncConfig::getRegistry();
@ -158,7 +192,8 @@ SyncConfig::SyncConfig(const string &peer,
boost::shared_ptr<ConfigTree> tree,
const string &redirectPeerRootPath) :
m_layout(SHARED_LAYOUT),
m_redirectPeerRootPath(redirectPeerRootPath)
m_redirectPeerRootPath(redirectPeerRootPath),
m_configWriteMode(defaultConfigWriteMode())
{
// initialize properties
SyncConfig::getRegistry();
@ -220,6 +255,7 @@ SyncConfig::SyncConfig(const string &peer,
m_contextNode = m_peerNode;
m_hiddenPeerNode =
m_contextHiddenNode =
m_globalHiddenNode =
node;
m_props[false] = m_peerNode;
m_props[true].reset(new FilterConfigNode(m_hiddenPeerNode));
@ -231,6 +267,9 @@ SyncConfig::SyncConfig(const string &peer,
path = "";
node = m_tree->open(path, ConfigTree::visible);
m_globalNode.reset(new FilterConfigNode(node));
node = m_tree->open(path, ConfigTree::hidden);
m_globalHiddenNode = node;
path = m_peerPath;
node = m_tree->open(path, ConfigTree::visible);
m_peerNode.reset(new FilterConfigNode(node));
@ -252,9 +291,16 @@ SyncConfig::SyncConfig(const string &peer,
m_peerNode);
mnode->setNode(false, ConfigProperty::NO_SHARING,
m_peerNode);
// no multiplexing necessary for hidden nodes
m_props[true].reset(new FilterConfigNode(m_hiddenPeerNode));
mnode.reset(new MultiplexConfigNode(m_peerNode->getName(),
getRegistry(),
true));
m_props[true] = mnode;
mnode->setNode(true, ConfigProperty::GLOBAL_SHARING,
m_globalHiddenNode);
mnode->setNode(true, ConfigProperty::SOURCE_SET_SHARING,
m_peerNode);
mnode->setNode(true, ConfigProperty::NO_SHARING,
m_peerNode);
break;
}
case SHARED_LAYOUT:
@ -262,6 +308,8 @@ SyncConfig::SyncConfig(const string &peer,
path = "";
node = m_tree->open(path, ConfigTree::visible);
m_globalNode.reset(new FilterConfigNode(node));
node = m_tree->open(path, ConfigTree::hidden);
m_globalHiddenNode = node;
path = m_peerPath;
if (path.empty()) {
@ -317,8 +365,137 @@ SyncConfig::SyncConfig(const string &peer,
m_contextHiddenNode);
mnode->setNode(true, ConfigProperty::NO_SHARING,
m_hiddenPeerNode);
mnode->setNode(true, ConfigProperty::GLOBAL_SHARING,
m_globalHiddenNode);
break;
}
// read version check
for (ConfigLevel level = CONFIG_LEVEL_ROOT;
level < CONFIG_LEVEL_MAX;
level = (ConfigLevel)(level + 1)) {
if (exists(level)) {
if (getConfigVersion(level, CONFIG_MIN_VERSION) > ConfigVersions[level][CONFIG_CUR_VERSION]) {
SE_LOG_INFO(NULL, NULL, "config version check failed: %s has format %d, but this SyncEvolution release only supports format %d",
ConfigLevel2String(level).c_str(),
getConfigVersion(level, CONFIG_MIN_VERSION),
ConfigVersions[level][CONFIG_CUR_VERSION]);
// our code is too old to read the config, reject it
SE_THROW_EXCEPTION_STATUS(StatusException,
StringPrintf("SyncEvolution %s is too old to read configuration '%s', please upgrade SyncEvolution.",
VERSION, peer.c_str()),
STATUS_RELEASE_TOO_OLD);
}
}
}
// Note that the version check does not reject old configs because
// they are too old; so far, any release must be able to read any
// older config.
}
void SyncConfig::prepareConfigForWrite()
{
// check versions before bumping to something incompatible with the
// previous user of the config
for (ConfigLevel level = CONFIG_LEVEL_ROOT;
level < CONFIG_LEVEL_MAX;
level = (ConfigLevel)(level + 1)) {
if (exists(level)) {
if (getConfigVersion(level, CONFIG_CUR_VERSION) < ConfigVersions[level][CONFIG_MIN_VERSION]) {
// release which created config will no longer be able to read
// updated config; either alert user or migrate automatically
string config;
switch (level) {
case CONFIG_LEVEL_CONTEXT:
config = getContextName();
break;
case CONFIG_LEVEL_PEER:
config = getConfigName();
break;
case CONFIG_LEVEL_ROOT:
case CONFIG_LEVEL_MAX:
// keep compiler happy, not reached for _MAX
break;
}
SE_LOG_INFO(NULL, NULL, "must change format of %s '%s' in backward-incompatible way",
ConfigLevel2String(level).c_str(),
config.c_str());
if (m_configWriteMode == MIGRATE_AUTOMATICALLY) {
// migrate config and anything beneath it,
// so no further checking needed
migrate(config);
break;
} else {
SE_THROW_EXCEPTION_STATUS(StatusException,
StringPrintf("Proceeding would modify config '%s' such "
"that the previous SyncEvolution release "
"will not be able to use it. Stopping now. "
"Please explicitly acknowledge this step by "
"running the following command on the command "
"line: syncevolution --migrate '%s'",
config.c_str(),
config.c_str()),
STATUS_MIGRATION_NEEDED);
}
}
}
}
// now set current versions at all levels,
// but without reducing versions: if a config has format
// "cur = 10", then properties or features added in that
// format remain even if the config is (temporarily?) used
// by a SyncEvolution binary which has "cur = 5".
for (ConfigLevel level = CONFIG_LEVEL_ROOT;
level < CONFIG_LEVEL_MAX;
level = (ConfigLevel)(level + 1)) {
if (level == CONFIG_LEVEL_PEER &&
m_peerPath.empty()) {
// no need (and no possibility) to set per-peer version)
break;
}
for (ConfigLimit limit = CONFIG_MIN_VERSION;
limit < CONFIG_VERSION_MAX;
limit = (ConfigLimit)(limit + 1)) {
// set if equal to ensure that version == 0 (the default)
// is set explicitly
if (getConfigVersion(level, limit) <= ConfigVersions[level][limit]) {
setConfigVersion(level, limit, ConfigVersions[level][limit]);
}
}
}
flush();
}
void SyncConfig::migrate(const std::string &config)
{
if (config.empty()) {
// migrating root not yet supported
SE_THROW("internal error, migrating config root not implemented");
} else {
// migrate using the higher-level logic in the Cmdline class
ostringstream out, err;
Cmdline migrate(out, err,
m_peer.c_str(),
"--migrate",
config.c_str(),
NULL);
bool res = migrate.parse() && migrate.run();
if (!res) {
if (!err.str().empty()) {
SE_LOG_ERROR(NULL, NULL, "%s", err.str().c_str());
}
if (!out.str().empty()) {
SE_LOG_INFO(NULL, NULL, "%s", out.str().c_str());
}
SE_THROW(StringPrintf("migration of config '%s' failed", config.c_str()));
}
// files that our tree access may have changed, refresh our
// in-memory copy
m_tree->reload();
}
}
string SyncConfig::getRootPath() const
@ -785,6 +962,23 @@ bool SyncConfig::exists() const
m_peerNode->exists();
}
bool SyncConfig::exists(ConfigLevel level) const
{
switch (level) {
case CONFIG_LEVEL_ROOT:
return m_globalNode->exists();
break;
case CONFIG_LEVEL_CONTEXT:
return m_contextNode->exists();
break;
case CONFIG_LEVEL_PEER:
return m_peerNode->exists();
break;
default:
return false;
}
}
string SyncConfig::getContextName() const
{
string peer, context;
@ -1310,6 +1504,49 @@ static SecondsConfigProperty syncPropAutoSyncDelay("autoSyncDelay",
"enough to complete the synchronization.\n",
"5M");
/* config and on-disk file versionsing */
static IntConfigProperty syncPropRootMinVersion("rootMinVersion", "");
static IntConfigProperty syncPropRootCurVersion("rootCurVersion", "");
static IntConfigProperty syncPropContextMinVersion("contextMinVersion", "");
static IntConfigProperty syncPropContextCurVersion("contextCurVersion", "");
static IntConfigProperty syncPropPeerMinVersion("peerMinVersion", "");
static IntConfigProperty syncPropPeerCurVersion("peerCurVersion", "");
static const IntConfigProperty *configVersioning[CONFIG_LEVEL_MAX][CONFIG_VERSION_MAX] = {
{ &syncPropRootMinVersion, &syncPropRootCurVersion },
{ &syncPropContextMinVersion, &syncPropContextCurVersion },
{ &syncPropPeerMinVersion, &syncPropPeerCurVersion }
};
static const IntConfigProperty &getConfigVersionProp(ConfigLevel level, ConfigLimit limit)
{
if (level < 0 || level >= CONFIG_LEVEL_MAX ||
limit < 0 || limit >= CONFIG_VERSION_MAX) {
SE_THROW("getConfigVersionProp: invalid args");
}
return *configVersioning[level][limit];
}
int SyncConfig::getConfigVersion(ConfigLevel level, ConfigLimit limit) const
{
const IntConfigProperty &prop = getConfigVersionProp(level, limit);
return prop.getPropertyValue(*getNode(prop));
}
void SyncConfig::setConfigVersion(ConfigLevel level, ConfigLimit limit, int version)
{
if (m_layout != SHARED_LAYOUT) {
// old-style layouts have version 0 by default, no need
// (and sometimes no possibility) to set this explicitly
if (version != 0) {
SE_THROW(StringPrintf("cannot bump config version in old-style config %s", m_peer.c_str()));
}
} else {
const IntConfigProperty &prop = getConfigVersionProp(level, limit);
prop.setProperty(*getNode(prop), version);
}
}
ConfigPropertyRegistry &SyncConfig::getRegistry()
{
static ConfigPropertyRegistry registry;
@ -1357,6 +1594,17 @@ ConfigPropertyRegistry &SyncConfig::getRegistry()
registry.push_back(&syncPropDeviceData);
registry.push_back(&syncPropDefaultPeer);
#if 0
// Must not be registered! Not valid for --sync-property and
// must not be copied between configs.
registry.push_back(&syncPropRootMinVersion);
registry.push_back(&syncPropRootCurVersion);
registry.push_back(&syncPropContextMinVersion);
registry.push_back(&syncPropContextCurVersion);
registry.push_back(&syncPropPeerMinVersion);
registry.push_back(&syncPropPeerCurVersion);
#endif
// obligatory sync properties
syncPropUsername.setObligatory(true);
syncPropPassword.setObligatory(true);
@ -1368,14 +1616,24 @@ ConfigPropertyRegistry &SyncConfig::getRegistry()
syncPropConfigDate.setHidden(true);
syncPropNonce.setHidden(true);
syncPropDeviceData.setHidden(true);
syncPropRootMinVersion.setHidden(true);
syncPropRootCurVersion.setHidden(true);
syncPropContextMinVersion.setHidden(true);
syncPropContextCurVersion.setHidden(true);
syncPropPeerMinVersion.setHidden(true);
syncPropPeerCurVersion.setHidden(true);
// global sync properties
syncPropDefaultPeer.setSharing(ConfigProperty::GLOBAL_SHARING);
syncPropRootMinVersion.setSharing(ConfigProperty::GLOBAL_SHARING);
syncPropRootCurVersion.setSharing(ConfigProperty::GLOBAL_SHARING);
// peer independent sync properties
syncPropLogDir.setSharing(ConfigProperty::SOURCE_SET_SHARING);
syncPropMaxLogDirs.setSharing(ConfigProperty::SOURCE_SET_SHARING);
syncPropDevID.setSharing(ConfigProperty::SOURCE_SET_SHARING);
syncPropContextMinVersion.setSharing(ConfigProperty::SOURCE_SET_SHARING);
syncPropContextCurVersion.setSharing(ConfigProperty::SOURCE_SET_SHARING);
initialized = true;
}
@ -1723,7 +1981,7 @@ SyncConfig::getNode(const ConfigProperty &prop)
switch (prop.getSharing()) {
case ConfigProperty::GLOBAL_SHARING:
if (prop.isHidden()) {
boost::shared_ptr<FilterConfigNode>(new FilterConfigNode(boost::shared_ptr<ConfigNode>(new DevNullConfigNode("no hidden global properties"))));
return boost::shared_ptr<FilterConfigNode>(new FilterConfigNode(m_globalHiddenNode));
} else {
return m_globalNode;
}

View File

@ -46,6 +46,79 @@ using namespace std;
* @{
*/
/**
* The SyncEvolution configuration is versioned, so that incompatible
* changes to the on-disk config and files can be made more reliably.
*
* The on-disk configuration is versioned at three levels:
* - root level
* - context
* - peer
*
* This granularity allows migrating individual peers, contexts or
* everything to a new format.
*
* For each of these levels, two numbers are stored on disk and
* hard-coded in the binary:
* - current version = incremented each time the format is extended
* - minimum version = set to current version each time a backwards
* incompatible change is made
*
* This mirrors the libtool library versioning.
*
* Reading must check that the on-disk minimum version is <= the
* binary's current version. Otherwise the config is too recent to
* be used.
*
* Writing will bump minimum and current version on disk to the
* versions in the binary. It will never decrease versions. This
* works when the more recent format adds information that can
* be safely ignored by older releases. If that is not possible,
* then the "minimum" version must be increased to prevent older
* releases from using the config.
*
* If bumping the versions increases the minimum version
* beyond the version supported by the release which wrote the config,
* that release will no longer work. Experimental releases will throw
* an error and users must explicitly migrate to the current
* format. Stable releases will migrate automatically.
*
* The on-disks current version can be checked to determine how to
* handle it. It may be more obvious to simple check for the existence
* of certain properties (that's how this was handled before the
* introduction of versioning).
*
* Here are some simple rules for handling the versions:
* - increase CUR version when adding new properties or files
* - set MIN to CUR when it is not safe that older releases
* read and write a config with the current format
*
* SyncEvolution < 1.2 had no versioning. It's format is 0.
*/
static const int CONFIG_ROOT_MIN_VERSION = 0;
static const int CONFIG_ROOT_CUR_VERSION = 0;
static const int CONFIG_CONTEXT_MIN_VERSION = 0;
static const int CONFIG_CONTEXT_CUR_VERSION = 0;
static const int CONFIG_PEER_MIN_VERSION = 0;
static const int CONFIG_PEER_CUR_VERSION = 0;
enum ConfigLevel {
CONFIG_LEVEL_ROOT, /**< = GLOBAL_SHARING */
CONFIG_LEVEL_CONTEXT, /**< = SOURCE_SET_SHARING */
CONFIG_LEVEL_PEER, /**< = NO_SHARING */
CONFIG_LEVEL_MAX
};
std::string ConfigLevel2String(ConfigLevel level);
enum ConfigLimit {
CONFIG_MIN_VERSION,
CONFIG_CUR_VERSION,
CONFIG_VERSION_MAX
};
extern int ConfigVersions[CONFIG_LEVEL_MAX][CONFIG_VERSION_MAX];
class SyncSourceConfig;
typedef SyncSourceConfig PersistentSyncSourceConfig;
class ConfigTree;
@ -834,6 +907,11 @@ class SyncConfig {
* places. Will succeed even if config does not
* yet exist: flushing such a config creates it.
*
* Does a version check to ensure that the config can be
* read. Users of the instance must to an explicit
* prepareConfigForWrite() if the config or the files associated
* with it (Synthesis bin files) are going to be written.
*
* @param peer string that identifies the peer,
* matching regex (.*)(@([^@]*))?
* where the $1 (the first part) is
@ -867,6 +945,31 @@ class SyncConfig {
*/
SyncConfig();
/**
* determines whether the need to migrate a config causes a
* STATUS_MIGRATION_NEEDED error or does the migration
* automatically; default is to migrate automatically in
* stable releases and to ask in development releases
*/
enum ConfigWriteMode {
MIGRATE_AUTOMATICALLY,
ASK_USER_TO_MIGRATE
};
ConfigWriteMode getConfigWriteMode() const { return m_configWriteMode; }
void setConfigWriteMode(ConfigWriteMode mode) { m_configWriteMode = mode; }
/**
* This does another version check which ensures that the config
* is not unintentionally altered so that it cannot be read by
* older SyncEvolution releases. If the config cannot be written
* without breaking older releases, then either the call will fail
* (development releases) or migrate the config (stable releases).
* Can be controlled via setConfigWriteMode();
*
* Also writes the current config versions into the config.
*/
void prepareConfigForWrite();
/** absolute directory name of the configuration root */
string getRootPath() const;
@ -991,9 +1094,20 @@ class SyncConfig {
*/
static boost::shared_ptr<SyncConfig> createPeerTemplate(const string &peer);
/** true if the main configuration file already exists */
/**
* true if the main configuration file already exists;
* "main" here means the per-peer config or context config,
* depending on what the config refers to
*/
bool exists() const;
/**
* true if the config files for the selected level exist;
* false is returned for CONFIG_LEVEL_PEER and a config
* which refers to a context
*/
bool exists(ConfigLevel level) const;
/**
* The normalized, unique config name used by this instance.
* Empty if not backed up by a real config.
@ -1417,6 +1531,16 @@ private:
const std::string &configname,
SyncConfig::ConfigList &res);
/* internal access to configuration versioning */
int getConfigVersion(ConfigLevel level, ConfigLimit limit) const;
void setConfigVersion(ConfigLevel level, ConfigLimit limit, int version);
/**
* migrate root (""), context or peer config and everything contained in them to
* the current config format
*/
void migrate(const std::string &config);
/**
* set tree and nodes to VolatileConfigTree/Node
*/
@ -1453,6 +1577,7 @@ private:
string m_redirectPeerRootPath;
string m_cachedPassword;
string m_cachedProxyPassword;
ConfigWriteMode m_configWriteMode;
/** holds all config nodes relative to the root that we found */
boost::shared_ptr<ConfigTree> m_tree;
@ -1460,7 +1585,8 @@ private:
/** access to global sync properties, independent of
the context (for example, "defaultPeer") */
boost::shared_ptr<FilterConfigNode> m_globalNode;
boost::shared_ptr<ConfigNode> m_globalHiddenNode;
/** access to properties shared between peers */
boost::shared_ptr<FilterConfigNode> m_contextNode;
boost::shared_ptr<ConfigNode> m_contextHiddenNode;

View File

@ -2783,6 +2783,17 @@ void SyncContext::initMain(const char *appname)
}
}
// TODO: set via gen-autotools and configure
static bool IsStableRelease = false;
bool SyncContext::isStableRelease()
{
return IsStableRelease;
}
void SyncContext::setStableRelease(bool isStableRelease)
{
IsStableRelease = isStableRelease;
}
SyncMLStatus SyncContext::sync(SyncReport *report)
{
SyncMLStatus status = STATUS_OK;
@ -2850,6 +2861,9 @@ SyncMLStatus SyncContext::sync(SyncReport *report)
SE_LOG_DEV(NULL, NULL, "%s", EDSAbiWrapperDebug());
SE_LOG_DEV(NULL, NULL, "%s", SyncSource::backendsDebug().c_str());
// ensure that config can be modified (might have to be migrated first)
prepareConfigForWrite();
// instantiate backends, but do not open them yet
initSources(sourceList);
if (sourceList.empty()) {

View File

@ -176,6 +176,17 @@ class SyncContext : public SyncConfig, public ConfigUserInterface {
*/
static void initMain(const char *appname);
/**
* true if binary was compiled as stable release
* (see gen-autotools.sh)
*/
static bool isStableRelease();
/**
* override stable release mode (for testing purposes)
*/
static void setStableRelease(bool isStableRelease);
/**
* SyncContext using a volatile config
* and no logging.

View File

@ -153,6 +153,20 @@ enum SyncMLStatus {
*/
STATUS_PASSWORD_TIMEOUT = 22003,
/**
* On-disk files (config, Synthesis binfiles) are too recent
* to be read and/or used by this SyncEvolution release.
* User must upgrade.
*/
STATUS_RELEASE_TOO_OLD = 22004,
/**
* On-disk files would be changed in such a way that downgrading
* becomes impossible. User must explicitly migrate config if
* he wants to proceed.
*/
STATUS_MIGRATION_NEEDED = 22005,
STATUS_MAX = 0x7FFFFFF
};