Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,12 @@
import com.cloud.utils.crypt.DBEncryptionUtil;
import com.cloud.utils.db.GlobalLock;
import com.cloud.utils.db.ScriptRunner;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;

import com.google.common.annotations.VisibleForTesting;

public class DatabaseUpgradeChecker implements SystemIntegrityChecker {
Expand Down Expand Up @@ -247,7 +251,6 @@ protected void runScript(Connection conn, InputStream file) {
LOGGER.error("Unable to execute upgrade script", e);
throw new CloudRuntimeException("Unable to execute upgrade script", e);
}

}

@VisibleForTesting
Expand Down Expand Up @@ -448,43 +451,101 @@ public void check() {
throw new CloudRuntimeException("Unable to acquire lock to check for database integrity.");
}

try {
initializeDatabaseEncryptors();

final CloudStackVersion dbVersion = CloudStackVersion.parse(_dao.getCurrentVersion());
final String currentVersionValue = this.getClass().getPackage().getImplementationVersion();
doUpgrades(lock);
} finally {
lock.releaseRef();
}
}

if (StringUtils.isBlank(currentVersionValue)) {
return;
boolean isStandalone() throws CloudRuntimeException {
return Transaction.execute(new TransactionCallback<>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
String sql = "SELECT COUNT(*) FROM `cloud`.`mshost` WHERE `state` = 'UP'";
try (Connection conn = TransactionLegacy.getStandaloneConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
int count = rs.getInt(1);
return count == 0;
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here has a subtle issue. The query counts management servers with state = 'UP', and returns true (standalone) when count is 0. However, this check is being performed by a management server that is currently running.

This means:

  1. If this MS has already registered itself in the mshost table as 'UP', the count would be at least 1, and it would never be considered standalone.
  2. If this MS hasn't registered yet, the count might be 0 even in a clustered environment if other MSes are down or not yet started.

The check should likely exclude the current management server from the count, or check for count <= 1 instead of count == 0, to properly detect if this is the only running MS.

Suggested change
return count == 0;
return count <= 1;

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@DaanHoogland DaanHoogland Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one valid issue is that an MS performing an upgrade is not yet registered as UP. so this fix is better than what we have but certainly not watertight yet.

}
} catch (SQLException e) {
String errorMessage = "Unable to check if the management server is running in standalone mode.";
LOGGER.error(errorMessage, e);
return false;
} catch (NullPointerException npe) {
String errorMessage = "Unable to check if the management server is running in standalone mode. Not able to get a Database connection.";
LOGGER.error(errorMessage, npe);
return false;
}
return true;
}
});
}

String csVersion = SystemVmTemplateRegistration.parseMetadataFile();
final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion);
final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue);
SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.valueOf(sysVmVersion.getMajorRelease()) + "." + String.valueOf(sysVmVersion.getMinorRelease());
SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease());
@VisibleForTesting
protected void doUpgrades(GlobalLock lock) {
try {
initializeDatabaseEncryptors();

LOGGER.info("DB version = " + dbVersion + " Code Version = " + currentVersion);
final CloudStackVersion dbVersion = CloudStackVersion.parse(_dao.getCurrentVersion());
final String currentVersionValue = getImplementationVersion();

if (dbVersion.compareTo(currentVersion) > 0) {
throw new CloudRuntimeException("Database version " + dbVersion + " is higher than management software version " + currentVersionValue);
}
if (StringUtils.isBlank(currentVersionValue)) {
return;
}

if (dbVersion.compareTo(currentVersion) == 0) {
LOGGER.info("DB version and code version matches so no upgrade needed.");
return;
}
String csVersion = parseSystemVmMetadata();
final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion);
final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue);
SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.valueOf(sysVmVersion.getMajorRelease()) + "." + String.valueOf(sysVmVersion.getMinorRelease());
SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease());

LOGGER.info("DB version = " + dbVersion + " Code Version = " + currentVersion);

if (dbVersion.compareTo(currentVersion) > 0) {
throw new CloudRuntimeException("Database version " + dbVersion + " is higher than management software version " + currentVersionValue);
}

if (dbVersion.compareTo(currentVersion) == 0) {
LOGGER.info("DB version and code version matches so no upgrade needed.");
return;
}

if (isStandalone()) {
upgrade(dbVersion, currentVersion);
} finally {
lock.unlock();
} else {
String errorMessage = "Database upgrade is required but the management server is running in a clustered environment. " +
"Please perform the database upgrade when the management server is not running in a clustered environment.";
LOGGER.error(errorMessage);
handleClusteredUpgradeRequired(); // allow tests to override behavior
}
} finally {
lock.releaseRef();
lock.unlock();
}
}

private void initializeDatabaseEncryptors() {
/**
* Hook that is called when an upgrade is required but the management server is clustered.
* Default behavior is to exit the JVM, tests can override to throw instead.
*/
@VisibleForTesting
protected void handleClusteredUpgradeRequired() {
System.exit(5); // I would prefer ServerDaemon.abort(errorMessage) but that would create a dependency hell
}

@VisibleForTesting
protected String getImplementationVersion() {
return this.getClass().getPackage().getImplementationVersion();
}

@VisibleForTesting
protected String parseSystemVmMetadata() {
return SystemVmTemplateRegistration.parseMetadataFile();
}

// Make this protected so tests can noop it out
protected void initializeDatabaseEncryptors() {
TransactionLegacy txn = TransactionLegacy.open("initializeDatabaseEncryptors");
txn.start();
String errorMessage = "Unable to get the database connections";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
package com.cloud.upgrade;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import com.cloud.upgrade.dao.VersionDao;
import com.cloud.upgrade.dao.VersionDaoImpl;
import com.cloud.upgrade.dao.VersionVO;
import com.cloud.utils.db.GlobalLock;
import org.junit.Test;

public class DatabaseUpgradeCheckerDoUpgradesTest {

static class StubVersionDao extends VersionDaoImpl implements VersionDao {
private final String currentVersion;

StubVersionDao(String currentVersion) {
this.currentVersion = currentVersion;
}

@Override
public VersionVO findByVersion(String version, VersionVO.Step step) {
return null;
}

@Override
public String getCurrentVersion() {
return currentVersion;
}

}

private static class TestableChecker extends DatabaseUpgradeChecker {
boolean initializeCalled = false;
boolean upgradeCalled = false;
boolean clusterHandlerCalled = false;
String implVersionOverride = null;
String sysVmMetadataOverride = "4.8.0";
boolean standaloneOverride = true;

TestableChecker(String daoVersion) {
// set a stub DAO
this._dao = new StubVersionDao(daoVersion);
}

@Override
protected void initializeDatabaseEncryptors() {
initializeCalled = true;
// noop instead of doing DB work
}

@Override
protected String getImplementationVersion() {
return implVersionOverride;
}

@Override
protected String parseSystemVmMetadata() {
return sysVmMetadataOverride;
}

@Override
boolean isStandalone() {
return standaloneOverride;
}

@Override
protected void upgrade(org.apache.cloudstack.utils.CloudStackVersion dbVersion, org.apache.cloudstack.utils.CloudStackVersion currentVersion) {
upgradeCalled = true;
}

@Override
protected void handleClusteredUpgradeRequired() {
clusterHandlerCalled = true;
}
}

@Test
public void testDoUpgrades_noImplementationVersion_returnsEarly() {
TestableChecker checker = new TestableChecker("4.8.0");
checker.implVersionOverride = ""; // blank -> should return early

GlobalLock lock = GlobalLock.getInternLock("test-noimpl");
try {
// acquire lock so doUpgrades can safely call unlock in finally
lock.lock(1);
checker.doUpgrades(lock);
} finally {
// ensure lock released if still held
lock.releaseRef();
}

assertTrue("initializeDatabaseEncryptors should be called before returning", checker.initializeCalled);
assertFalse("upgrade should not be called when implementation version is blank", checker.upgradeCalled);
assertFalse("cluster handler should not be called", checker.clusterHandlerCalled);
}

@Test
public void testDoUpgrades_dbUpToDate_noUpgrade() {
// DB version = code version -> no upgrade
TestableChecker checker = new TestableChecker("4.8.1");
checker.implVersionOverride = "4.8.1";
checker.sysVmMetadataOverride = "4.8.1";

GlobalLock lock = GlobalLock.getInternLock("test-uptodate");
try {
lock.lock(1);
checker.doUpgrades(lock);
} finally {
lock.releaseRef();
}

assertTrue(checker.initializeCalled);
assertFalse(checker.upgradeCalled);
assertFalse(checker.clusterHandlerCalled);
}

@Test
public void testDoUpgrades_requiresUpgrade_standalone_invokesUpgrade() {
TestableChecker checker = new TestableChecker("4.8.0");
checker.implVersionOverride = "4.8.2"; // code is newer than DB
checker.sysVmMetadataOverride = "4.8.2";
checker.standaloneOverride = true;

GlobalLock lock = GlobalLock.getInternLock("test-upgrade-standalone");
try {
lock.lock(1);
checker.doUpgrades(lock);
} finally {
lock.releaseRef();
}

assertTrue(checker.initializeCalled);
assertTrue("upgrade should be invoked in standalone mode", checker.upgradeCalled);
assertFalse(checker.clusterHandlerCalled);
}

@Test
public void testDoUpgrades_requiresUpgrade_clustered_invokesHandler() {
TestableChecker checker = new TestableChecker("4.8.0");
checker.implVersionOverride = "4.8.2"; // code is newer than DB
checker.sysVmMetadataOverride = "4.8.2";
checker.standaloneOverride = false;

GlobalLock lock = GlobalLock.getInternLock("test-upgrade-clustered");
try {
lock.lock(1);
checker.doUpgrades(lock);
} finally {
lock.releaseRef();
}

assertTrue(checker.initializeCalled);
assertFalse("upgrade should not be invoked in clustered mode", checker.upgradeCalled);
assertTrue("cluster handler should be invoked in clustered mode", checker.clusterHandlerCalled);
}
}
Loading
Loading