/** * Copyright (C) 2012 10gen Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the GNU Affero General Public License in all respects * for all of the code used other than as permitted herein. If you modify * file(s) with this exception, you may extend this exception to your * version of the file(s), but you are not obligated to do so. If you do not * wish to do so, delete this exception statement from your version. If you * delete this exception statement from all source files in the program, * then also delete it in the license file. */ #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kSharding #include "mongo/platform/basic.h" #include "mongo/s/catalog/legacy/config_upgrade.h" #include "mongo/client/connpool.h" #include "mongo/client/dbclientcursor.h" #include "mongo/client/syncclusterconnection.h" #include "mongo/s/catalog/catalog_manager.h" #include "mongo/s/catalog/legacy/cluster_client_internal.h" #include "mongo/s/catalog/type_collection.h" #include "mongo/s/catalog/type_database.h" #include "mongo/s/catalog/type_settings.h" #include "mongo/s/catalog/type_shard.h" #include "mongo/s/catalog/dist_lock_manager.h" #include "mongo/s/mongo_version_range.h" #include "mongo/s/type_config_version.h" #include "mongo/stdx/functional.h" #include "mongo/util/assert_util.h" #include "mongo/util/log.h" #include "mongo/util/version.h" namespace mongo { using std::unique_ptr; using std::make_pair; using std::map; using std::string; using std::vector; using str::stream; // Implemented in the respective steps' .cpp file bool doUpgradeV0ToV7(CatalogManager* catalogManager, const VersionType& lastVersionInfo, std::string* errMsg); bool doUpgradeV6ToV7(CatalogManager* catalogManager, const VersionType& lastVersionInfo, std::string* errMsg); namespace { struct VersionRange { VersionRange(int _minCompatibleVersion, int _currentVersion) : minCompatibleVersion(_minCompatibleVersion), currentVersion(_currentVersion) {} bool operator==(const VersionRange& other) const { return (other.minCompatibleVersion == minCompatibleVersion) && (other.currentVersion == currentVersion); } bool operator!=(const VersionRange& other) const { return !(*this == other); } int minCompatibleVersion; int currentVersion; }; enum VersionStatus { // No way to upgrade the test version to be compatible with current version VersionStatus_Incompatible, // Current version is compatible with test version VersionStatus_Compatible, // Test version must be upgraded to be compatible with current version VersionStatus_NeedUpgrade }; /** * Encapsulates the information needed to register a config upgrade. */ struct UpgradeStep { typedef stdx::function UpgradeCallback; UpgradeStep(int _fromVersion, const VersionRange& _toVersionRange, UpgradeCallback _upgradeCallback) : fromVersion(_fromVersion), toVersionRange(_toVersionRange), upgradeCallback(_upgradeCallback) {} // The config version we're upgrading from int fromVersion; // The config version we're upgrading to and the min compatible config version (min, to) VersionRange toVersionRange; // The upgrade callback which performs the actual upgrade UpgradeCallback upgradeCallback; }; typedef map ConfigUpgradeRegistry; /** * Does a sanity-check validation of the registry ensuring three things: * 1. All upgrade paths lead to the same minCompatible/currentVersion * 2. Our constants match this final version pair * 3. There is a zero-version upgrade path */ void validateRegistry(const ConfigUpgradeRegistry& registry) { VersionRange maxCompatibleConfigVersionRange(-1, -1); bool hasZeroVersionUpgrade = false; for (const auto& upgradeStep : registry) { const UpgradeStep& upgrade = upgradeStep.second; if (upgrade.fromVersion == 0) { hasZeroVersionUpgrade = true; } if (maxCompatibleConfigVersionRange.currentVersion < upgrade.toVersionRange.currentVersion) { maxCompatibleConfigVersionRange = upgrade.toVersionRange; } else if (maxCompatibleConfigVersionRange.currentVersion == upgrade.toVersionRange.currentVersion) { // Make sure all max upgrade paths end up with same version and compatibility fassert(16621, maxCompatibleConfigVersionRange == upgrade.toVersionRange); } } // Make sure we have a zero-version upgrade fassert(16622, hasZeroVersionUpgrade); // Make sure our max registered range is the same as our constants fassert(16623, maxCompatibleConfigVersionRange == VersionRange(MIN_COMPATIBLE_CONFIG_VERSION, CURRENT_CONFIG_VERSION)); } /** * Creates a registry of config upgrades used by the code below. * * MODIFY THIS CODE HERE TO CREATE A NEW UPGRADE PATH FROM X to Y * YOU MUST ALSO MODIFY THE VERSION DECLARATIONS IN config_upgrade.h * * Caveats: * - All upgrade paths must eventually lead to the exact same version range of * min and max compatible versions. * - This resulting version range must be equal to: * make_pair(MIN_COMPATIBLE_CONFIG_VERSION, CURRENT_CONFIG_VERSION) * - There must always be an upgrade path from the empty version (0) to the latest * config version. * * If any of the above is false, we fassert and fail to start. */ ConfigUpgradeRegistry createRegistry() { ConfigUpgradeRegistry registry; // v0 to v7 UpgradeStep v0ToV7(0, VersionRange(6, 7), doUpgradeV0ToV7); registry.insert(make_pair(v0ToV7.fromVersion, v0ToV7)); // v6 to v7 UpgradeStep v6ToV7(6, VersionRange(6, 7), doUpgradeV6ToV7); registry.insert(make_pair(v6ToV7.fromVersion, v6ToV7)); validateRegistry(registry); return registry; } /** * Checks whether or not a particular cluster version is compatible with our current * version and mongodb version. The version is compatible if it falls between the * MIN_COMPATIBLE_CONFIG_VERSION and CURRENT_CONFIG_VERSION and is not explicitly excluded. * * @return a VersionStatus enum indicating compatibility */ VersionStatus isConfigVersionCompatible(const VersionType& versionInfo, string* whyNot) { string dummy; if (!whyNot) { whyNot = &dummy; } // Check if we're empty if (versionInfo.getCurrentVersion() == UpgradeHistory_EmptyVersion) { return VersionStatus_NeedUpgrade; } // Check that we aren't too old if (CURRENT_CONFIG_VERSION < versionInfo.getMinCompatibleVersion()) { *whyNot = stream() << "the config version " << CURRENT_CONFIG_VERSION << " of our process is too old " << "for the detected config version " << versionInfo.getMinCompatibleVersion(); return VersionStatus_Incompatible; } // Check that the mongo version of this process hasn't been excluded from the cluster vector excludedRanges; if (versionInfo.isExcludingMongoVersionsSet() && !MongoVersionRange::parseBSONArray( versionInfo.getExcludingMongoVersions(), &excludedRanges, whyNot)) { *whyNot = stream() << "could not understand excluded version ranges" << causedBy(whyNot); return VersionStatus_Incompatible; } // versionString is the global version of this process if (isInMongoVersionRanges(versionString, excludedRanges)) { // Cast needed here for MSVC compiler issue *whyNot = stream() << "not compatible with current config version, version " << reinterpret_cast(versionString) << "has been excluded."; return VersionStatus_Incompatible; } // Check if we need to upgrade if (versionInfo.getCurrentVersion() >= CURRENT_CONFIG_VERSION) { return VersionStatus_Compatible; } return VersionStatus_NeedUpgrade; } // Checks that all config servers are online bool _checkConfigServersAlive(const ConnectionString& configLoc, string* errMsg) { bool resultOk; BSONObj result; try { ScopedDbConnection conn(configLoc, 30); if (conn->type() == ConnectionString::SYNC) { // TODO: Dynamic cast is bad, we need a better way of managing this op // via the heirarchy (or not) SyncClusterConnection* scc = dynamic_cast(conn.get()); fassert(16729, scc != NULL); return scc->prepare(*errMsg); } else { resultOk = conn->runCommand("admin", BSON("fsync" << 1), result); } conn.done(); } catch (const DBException& e) { *errMsg = e.toString(); return false; } if (!resultOk) { *errMsg = DBClientWithCommands::getLastErrorString(result); return false; } return true; } // Dispatches upgrades based on version to the upgrades registered in the upgrade registry bool _nextUpgrade(CatalogManager* catalogManager, const ConfigUpgradeRegistry& registry, const VersionType& lastVersionInfo, VersionType* upgradedVersionInfo, string* errMsg) { int fromVersion = lastVersionInfo.getCurrentVersion(); ConfigUpgradeRegistry::const_iterator foundIt = registry.find(fromVersion); if (foundIt == registry.end()) { *errMsg = stream() << "newer version " << CURRENT_CONFIG_VERSION << " of mongo config metadata is required, " << "current version is " << fromVersion << ", " << "don't know how to upgrade from this version"; return false; } const UpgradeStep& upgrade = foundIt->second; int toVersion = upgrade.toVersionRange.currentVersion; log() << "starting next upgrade step from v" << fromVersion << " to v" << toVersion; // Log begin to config.changelog catalogManager->logChange(NULL, "starting upgrade of config database", VersionType::ConfigNS, BSON("from" << fromVersion << "to" << toVersion)); if (!upgrade.upgradeCallback(catalogManager, lastVersionInfo, errMsg)) { *errMsg = stream() << "error upgrading config database from v" << fromVersion << " to v" << toVersion << causedBy(errMsg); return false; } // Get the config version we've upgraded to and make sure it's sane Status verifyConfigStatus = getConfigVersion(catalogManager, upgradedVersionInfo); if (!verifyConfigStatus.isOK()) { *errMsg = stream() << "failed to validate v" << fromVersion << " config version upgrade" << causedBy(verifyConfigStatus); return false; } catalogManager->logChange(NULL, "finished upgrade of config database", VersionType::ConfigNS, BSON("from" << fromVersion << "to" << toVersion)); return true; } } // namespace /** * Returns the config version of the cluster pointed at by the connection string. * * @return OK if version found successfully, error status if something bad happened. */ Status getConfigVersion(CatalogManager* catalogManager, VersionType* versionInfo) { try { versionInfo->clear(); ScopedDbConnection conn(catalogManager->connectionString(), 30); unique_ptr cursor(_safeCursor(conn->query("config.version", BSONObj()))); bool hasConfigData = conn->count(ShardType::ConfigNS) || conn->count(DatabaseType::ConfigNS) || conn->count(CollectionType::ConfigNS); if (!cursor->more()) { // Version is 1 if we have data, 0 if we're completely empty if (hasConfigData) { versionInfo->setMinCompatibleVersion(UpgradeHistory_UnreportedVersion); versionInfo->setCurrentVersion(UpgradeHistory_UnreportedVersion); } else { versionInfo->setMinCompatibleVersion(UpgradeHistory_EmptyVersion); versionInfo->setCurrentVersion(UpgradeHistory_EmptyVersion); } conn.done(); return Status::OK(); } BSONObj versionDoc = cursor->next(); string errMsg; if (!versionInfo->parseBSON(versionDoc, &errMsg) || !versionInfo->isValid(&errMsg)) { conn.done(); return Status(ErrorCodes::UnsupportedFormat, stream() << "invalid config version document " << versionDoc << causedBy(errMsg)); } if (cursor->more()) { conn.done(); return Status(ErrorCodes::RemoteValidationError, stream() << "should only have 1 document " << "in config.version collection"); } conn.done(); } catch (const DBException& e) { return e.toStatus(); } return Status::OK(); } bool checkAndUpgradeConfigVersion(CatalogManager* catalogManager, bool upgrade, VersionType* initialVersionInfo, VersionType* versionInfo, string* errMsg) { string dummy; if (!errMsg) { errMsg = &dummy; } Status getConfigStatus = getConfigVersion(catalogManager, versionInfo); if (!getConfigStatus.isOK()) { *errMsg = stream() << "could not load config version for upgrade" << causedBy(getConfigStatus); return false; } versionInfo->cloneTo(initialVersionInfo); VersionStatus comp = isConfigVersionCompatible(*versionInfo, errMsg); if (comp == VersionStatus_Incompatible) return false; if (comp == VersionStatus_Compatible) return true; invariant(comp == VersionStatus_NeedUpgrade); // // Our current config version is now greater than the current version, so we should upgrade // if possible. // // The first empty version is technically an upgrade, but has special semantics bool isEmptyVersion = versionInfo->getCurrentVersion() == UpgradeHistory_EmptyVersion; // First check for the upgrade flag (but no flag is needed if we're upgrading from empty) if (!isEmptyVersion && !upgrade) { *errMsg = stream() << "newer version " << CURRENT_CONFIG_VERSION << " of mongo config metadata is required, " << "current version is " << versionInfo->getCurrentVersion() << ", " << "need to run mongos with --upgrade"; return false; } // Contact the config servers to make sure all are online - otherwise we wait a long time // for locks. if (!_checkConfigServersAlive(catalogManager->connectionString(), errMsg)) { if (isEmptyVersion) { *errMsg = stream() << "all config servers must be reachable for initial" << " config database creation" << causedBy(errMsg); } else { *errMsg = stream() << "all config servers must be reachable for config upgrade" << causedBy(errMsg); } return false; } // Check whether or not the balancer is online, if it is online we will not upgrade // (but we will initialize the config server) if (!isEmptyVersion) { auto balSettingsResult = catalogManager->getGlobalSettings(SettingsType::BalancerDocKey); if (balSettingsResult.isOK()) { SettingsType balSettings = balSettingsResult.getValue(); if (!balSettings.getBalancerStopped()) { *errMsg = stream() << "balancer must be stopped for config upgrade" << causedBy(errMsg); } } } // // Acquire a lock for the upgrade process. // // We want to ensure that only a single mongo process is upgrading the config server at a // time. // string whyMessage(stream() << "upgrading config database to new format v" << CURRENT_CONFIG_VERSION); auto lockTimeout = stdx::chrono::milliseconds(20 * 60 * 1000); auto scopedDistLock = catalogManager->getDistLockManager()->lock("configUpgrade", whyMessage, lockTimeout); if (!scopedDistLock.isOK()) { *errMsg = scopedDistLock.getStatus().toString(); return false; } // // Double-check compatibility inside the upgrade lock // Another process may have won the lock earlier and done the upgrade for us, check // if this is the case. // getConfigStatus = getConfigVersion(catalogManager, versionInfo); if (!getConfigStatus.isOK()) { *errMsg = stream() << "could not reload config version for upgrade" << causedBy(getConfigStatus); return false; } versionInfo->cloneTo(initialVersionInfo); comp = isConfigVersionCompatible(*versionInfo, errMsg); if (comp == VersionStatus_Incompatible) return false; if (comp == VersionStatus_Compatible) return true; invariant(comp == VersionStatus_NeedUpgrade); // // Run through the upgrade steps necessary to bring our config version to the current // version // log() << "starting upgrade of config server from v" << versionInfo->getCurrentVersion() << " to v" << CURRENT_CONFIG_VERSION; ConfigUpgradeRegistry registry(createRegistry()); while (versionInfo->getCurrentVersion() < CURRENT_CONFIG_VERSION) { int fromVersion = versionInfo->getCurrentVersion(); // // Run the next upgrade process and replace versionInfo with the result of the // upgrade. // if (!_nextUpgrade(catalogManager, registry, *versionInfo, versionInfo, errMsg)) { return false; } // Ensure we're making progress here if (versionInfo->getCurrentVersion() <= fromVersion) { *errMsg = stream() << "bad v" << fromVersion << " config version upgrade, " << "version did not increment and is now " << versionInfo->getCurrentVersion(); return false; } } invariant(versionInfo->getCurrentVersion() == CURRENT_CONFIG_VERSION); log() << "upgrade of config server to v" << versionInfo->getCurrentVersion() << " successful"; return true; } } // namespace mongo