From ccc870cc2b25b9763cf69ffee0e33c9cb2b0c166 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 7 Sep 2026 16:57:30 +0530 Subject: [PATCH 01/15] engine-schema: upgrade path for 24.0.0 Adds a 4.23.0 to 24.0.0 upgrade path. Signed-off-by: Abhishek Kumar --- .../cloud/upgrade/DatabaseUpgradeChecker.java | 6 ++-- .../cloud/upgrade/dao/Upgrade42300to2400.java | 30 +++++++++++++++++++ .../db/schema-42300to2400-cleanup.sql | 20 +++++++++++++ .../META-INF/db/schema-42300to2400.sql | 20 +++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java create mode 100644 engine/schema/src/main/resources/META-INF/db/schema-42300to2400-cleanup.sql create mode 100644 engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index c3a982aa70e5..a431b63deabb 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -91,11 +91,12 @@ import com.cloud.upgrade.dao.Upgrade42020to42030; import com.cloud.upgrade.dao.Upgrade42030to42040; import com.cloud.upgrade.dao.Upgrade42040to42100; -import com.cloud.upgrade.dao.Upgrade42100to42200; -import com.cloud.upgrade.dao.Upgrade42200to42210; import com.cloud.upgrade.dao.Upgrade420to421; +import com.cloud.upgrade.dao.Upgrade42100to42200; import com.cloud.upgrade.dao.Upgrade421to430; +import com.cloud.upgrade.dao.Upgrade42200to42210; import com.cloud.upgrade.dao.Upgrade42210to42300; +import com.cloud.upgrade.dao.Upgrade42300to2400; import com.cloud.upgrade.dao.Upgrade430to440; import com.cloud.upgrade.dao.Upgrade431to440; import com.cloud.upgrade.dao.Upgrade432to440; @@ -248,6 +249,7 @@ public DatabaseUpgradeChecker() { .next("4.21.0.0", new Upgrade42100to42200()) .next("4.22.0.0", new Upgrade42200to42210()) .next("4.22.1.0", new Upgrade42210to42300()) + .next("4.23.0.0", new Upgrade42300to2400()) .build(); } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java new file mode 100644 index 000000000000..ce217cef9e75 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java @@ -0,0 +1,30 @@ +// 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.dao; + +public class Upgrade42300to2400 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { + + @Override + public String[] getUpgradableVersionRange() { + return new String[]{"4.23.0.0", "24.0.0"}; + } + + @Override + public String getUpgradedVersion() { + return "24.0.0"; + } +} diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400-cleanup.sql new file mode 100644 index 000000000000..861a038fe7a7 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400-cleanup.sql @@ -0,0 +1,20 @@ +-- 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. + +--; +-- Schema upgrade cleanup from 4.23.0.0 to 24.0.0 +--; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql new file mode 100644 index 000000000000..7c11013a17d2 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -0,0 +1,20 @@ +-- 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. + +--; +-- Schema upgrade from 4.23.0.0 to 24.0.0 +--; From 3dbe6566530bc715ef0739bf2a391f7c91a2f2de Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 9 Sep 2026 18:47:00 +0530 Subject: [PATCH 02/15] fix CS version Signed-off-by: Abhishek Kumar --- .../cloud/upgrade/DatabaseUpgradeChecker.java | 9 ++- .../cloudstack/veeam/api/dto/Version.java | 8 ++- .../cloudstack/utils/CloudStackVersion.java | 62 +++++++++++++++---- .../utils/CloudStackVersionTest.java | 43 +++++++++++-- 4 files changed, 102 insertions(+), 20 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index a431b63deabb..f3ec76a27c7a 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -515,8 +515,13 @@ protected void doUpgrades(GlobalLock lock) { String csVersion = parseSystemVmMetadata(); final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); - SystemVmTemplateRegistration.CS_MAJOR_VERSION = sysVmVersion.getMajorRelease() + "." + sysVmVersion.getMinorRelease(); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); + if (sysVmVersion.usesNewVersioning()) { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); + } else { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); + } LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java index 7b7d80a0f16c..2d14443e2d58 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java @@ -87,8 +87,12 @@ public static Version fromPackageAndCSVersion(boolean complete) { } version.setMajor(String.valueOf(csVersion.getMajorRelease())); version.setMinor(String.valueOf(csVersion.getMinorRelease())); - version.setBuild(String.valueOf(csVersion.getPatchRelease())); - version.setRevision(String.valueOf(csVersion.getSecurityRelease())); + if (csVersion.usesNewVersioning()) { + version.setBuild(String.valueOf(csVersion.getSecurityRelease())); + } else { + version.setBuild(String.valueOf(csVersion.getPatchRelease())); + version.setRevision(String.valueOf(csVersion.getSecurityRelease())); + } return version; } } diff --git a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java index e29bd9c4e17b..8eb4c6ab9289 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java @@ -39,22 +39,23 @@ */ public final class CloudStackVersion implements Comparable { - private final static Pattern NUMBER_VERSION_FORMAT = Pattern.compile("(\\d+\\.){2}(\\d+\\.)?\\d+"); - private final static Pattern FULL_VERSION_FORMAT = Pattern.compile("(\\d+\\.){2}(\\d+\\.)?\\d+(-[a-zA-Z]+)?(-\\d+)?(-SNAPSHOT)?"); + private final static Pattern NUMBER_VERSION_FORMAT = Pattern.compile("\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?"); + private final static Pattern FULL_VERSION_FORMAT = Pattern.compile("\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?(?:-[a-zA-Z]+)?(?:-\\d+)?(?:-SNAPSHOT)?"); + private final static int NEW_VERSIONING_CUTOVER_MAJOR_VERSION = 24; private final int majorRelease; private final int minorRelease; - private final int patchRelease; + private final Integer patchRelease; private final Integer securityRelease; - private CloudStackVersion(final int majorRelease, final int minorRelease, final int patchRelease, final Integer securityRelease) { + private CloudStackVersion(final int majorRelease, final int minorRelease, final Integer patchRelease, final Integer securityRelease) { super(); checkArgument(majorRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a majorRelease greater than 0."); checkArgument(minorRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a minorRelease greater than 0."); - checkArgument(patchRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a patchRelease greater than 0."); - checkArgument((securityRelease != null && securityRelease >= 0) || (securityRelease == null), + checkArgument(patchRelease == null || patchRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a patchRelease greater than 0."); + checkArgument(securityRelease == null || securityRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a null securityRelease or a non-null value greater than 0."); this.majorRelease = majorRelease; @@ -69,11 +70,13 @@ private CloudStackVersion(final int majorRelease, final int minorRelease, final * Parses a String representation of a version that conforms one of the following * formats into a CloudStackVersion instance: *
    - *
  • <major>.<minor>.<patch>.<security>
  • - *
  • <major>.<minor>.<patch>.<security>.<security>
  • - *
  • <major>.<minor>.<patch>.<security>.<security>-<any string>
  • + *
  • <major>.<minor>.<patch> (legacy, deprecated since 24.0.0, allowed only below major version 24)
  • + *
  • <major>.<minor>.<patch>.<security> (legacy, deprecated since 24.0.0, allowed only below major version 24)
  • + *
  • <major>.<minor>.<security release> (for versions >= 24.0.0)
  • *
* + * Legacy patch-based formats remain supported for backward compatibility. + * * If the string contains a suffix that begins with a "-" character, then the "-" and all characters following it * will be dropped. * @@ -91,7 +94,7 @@ public static CloudStackVersion parse(final String value) { checkArgument(StringUtils.isNotBlank(trimmedValue), CloudStackVersion.class.getName() + ".parse(String) requires a non-blank value"); checkArgument(NUMBER_VERSION_FORMAT.matcher(trimmedValue).matches(), CloudStackVersion.class.getName() + ".parse(String) passed " + - value + ", but requires a value in the format of int.int.int(.int)(-)"); + value + ", but requires a value in the format of int.int.int(.int)(-)"); final String[] components = trimmedValue.split("\\."); @@ -100,8 +103,26 @@ public static CloudStackVersion parse(final String value) { final int majorRelease = Integer.valueOf(components[0]); final int minorRelease = Integer.valueOf(components[1]); - final int patchRelease = Integer.valueOf(components[2]); - final Integer securityRelease = components.length == 3 ? null : Integer.valueOf(components[3]); + final int thirdComponent = Integer.valueOf(components[2]); + + final int patchRelease; + final Integer securityRelease; + + if (components.length == 4) { + checkArgument(isLegacyVersioning(majorRelease), CloudStackVersion.class.getName() + ".parse(String) passed " + value + + ", but major versions at or above 24 do not support legacy int.int.int.int format"); + // Deprecated legacy format: major.minor.patch.security + patchRelease = thirdComponent; + securityRelease = Integer.valueOf(components[3]); + } else if (isNewVersioning(majorRelease)) { + // New format: major.minor.securityRelease (patch dropped) + patchRelease = 0; + securityRelease = thirdComponent; + } else { + // Deprecated legacy format: major.minor.patch + patchRelease = thirdComponent; + securityRelease = null; + } return new CloudStackVersion(majorRelease, minorRelease, patchRelease, securityRelease); @@ -207,6 +228,14 @@ private static ImmutableList normalizeVersionValues(final ImmutableList } + private static boolean isLegacyVersioning(final int majorRelease) { + return majorRelease < NEW_VERSIONING_CUTOVER_MAJOR_VERSION; + } + + private static boolean isNewVersioning(final int majorRelease) { + return majorRelease >= NEW_VERSIONING_CUTOVER_MAJOR_VERSION; + } + /** * * @return The components of this version as an {@link ImmutableList} in order of major release, minor release, @@ -244,6 +273,10 @@ public Integer getSecurityRelease() { return securityRelease; } + public boolean usesNewVersioning() { + return isNewVersioning(majorRelease); + } + @Override public boolean equals(final Object thatObject) { @@ -270,6 +303,11 @@ public int hashCode() { @Override public String toString() { + // Canonicalize cutover-and-later versions to major.minor.securityRelease. + if (securityRelease != null && patchRelease == 0 && isNewVersioning(majorRelease)) { + return Joiner.on(".").join(ImmutableList.of(majorRelease, minorRelease, securityRelease)); + } + return Joiner.on(".").join(asList()); } diff --git a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java index dabaf9bc97d3..4d0b4cb0439b 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java @@ -36,7 +36,11 @@ public final class CloudStackVersionTest { "1.2.3, 1.2.3", "1.2.3.4, 1.2.3.4", "1.2.3-12, 1.2.3", - "1.2.3.4-14, 1.2.3.4" + "1.2.3.4-14, 1.2.3.4", + "23.9.5, 23.9.5", + "24.0.0, 24.0.0", + "24.0.1, 24.0.1", + "25.1.1, 25.1.1" }) public void testValidParse(final String inputValue, final String expectedVersion) { final CloudStackVersion version = CloudStackVersion.parse(inputValue); @@ -44,6 +48,28 @@ public void testValidParse(final String inputValue, final String expectedVersion assertEquals(expectedVersion, version.toString()); } + @Test + public void testParseComponentMappingForLegacyAndNewVersioning() { + final CloudStackVersion legacyVersion = CloudStackVersion.parse("23.9.5"); + assertEquals(23, legacyVersion.getMajorRelease()); + assertEquals(9, legacyVersion.getMinorRelease()); + assertEquals(5, legacyVersion.getPatchRelease()); + Assert.assertNull(legacyVersion.getSecurityRelease()); + + final CloudStackVersion newVersion = CloudStackVersion.parse("24.0.1"); + assertEquals(24, newVersion.getMajorRelease()); + assertEquals(0, newVersion.getMinorRelease()); + // Patch is retained as 0 to represent "no patch" in the new major.minor.security scheme. + assertEquals(0, newVersion.getPatchRelease()); + assertEquals(Integer.valueOf(1), newVersion.getSecurityRelease()); + + final CloudStackVersion futureNewVersion = CloudStackVersion.parse("25.1.1"); + assertEquals(25, futureNewVersion.getMajorRelease()); + assertEquals(1, futureNewVersion.getMinorRelease()); + assertEquals(0, futureNewVersion.getPatchRelease()); + assertEquals(Integer.valueOf(1), futureNewVersion.getSecurityRelease()); + } + @Test(expected = IllegalArgumentException.class) @DataProvider({ "1.2", @@ -52,7 +78,10 @@ public void testValidParse(final String inputValue, final String expectedVersion "aaaa", "", " ", - "1.2.3.4.5" + "1.2.3.4.5", + "24.0.0.1", + "25.0.0.1", + "26.2.3.4" }) public void testInvalidParse(final String invalidValue) { CloudStackVersion.parse(invalidValue); @@ -147,7 +176,9 @@ public void testEqualCompareDirect(final String value, final String thatValue) { "1.2.3.4-10, 1.0.0.0-5", "1.2.3-10, 1.0.0-5", "1.2.3.4, 1.0.0.0-5", - "1.2.3.4-10, 1.0.0" + "1.2.3.4-10, 1.0.0", + "24.0.2, 24.0.1", + "24.1.0, 24.0.9" }) public void testGreaterThanAndLessThanCompareTo(final String value, final String thatValue) { @@ -178,7 +209,9 @@ public void testGreaterThanAndLessThanCompareTo(final String value, final String "1.2.3.4-10, 1.0.0.0-5", "1.2.3-10, 1.0.0-5", "1.2.3.4, 1.0.0.0-5", - "1.2.3.4-10, 1.0.0" + "1.2.3.4-10, 1.0.0", + "24.0.2, 24.0.1", + "24.1.0, 24.0.9" }) public void testGreaterThanAndLessThanCompareDirect(final String value, final String thatValue) { @@ -213,6 +246,7 @@ private void verifyGetVMwareParentVersion(String hypervisorVersion, String expec Assert.assertEquals(CloudStackVersion.getVMwareParentVersion(hypervisorVersion), expectedParentVersion); } } + @Test public void testGetParentVersion() { verifyGetVMwareParentVersion(null, null); @@ -223,5 +257,6 @@ public void testGetParentVersion() { verifyGetVMwareParentVersion("8.0.0", "8.0"); verifyGetVMwareParentVersion("8.0.0.2", "8.0"); verifyGetVMwareParentVersion("8.0.1.0", "8.0.1"); + verifyGetVMwareParentVersion("24.1.1", "24.1"); } } From b4256869f5b265fbdad04702d46b4f26140e0dc6 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 10 Sep 2026 13:28:02 +0530 Subject: [PATCH 03/15] fix upgrade unit tests for cutover Signed-off-by: Abhishek Kumar --- .../upgrade/DatabaseUpgradeCheckerTest.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index 884398cf410d..763f78fe6949 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -25,6 +25,7 @@ import javax.sql.DataSource; import org.apache.cloudstack.utils.CloudStackVersion; +import org.checkerframework.checker.nullness.qual.NonNull; import org.junit.Test; import org.junit.Before; import org.junit.After; @@ -214,10 +215,10 @@ public void testFindUpgradePath452to490() { @Test public void testCalculateUpgradePathUnknownDbVersion() { - final CloudStackVersion dbVersion = CloudStackVersion.parse("4.99.0.0"); + final CloudStackVersion dbVersion = CloudStackVersion.parse("99.0.0"); assertNotNull(dbVersion); - final CloudStackVersion currentVersion = CloudStackVersion.parse("4.99.1.0"); + final CloudStackVersion currentVersion = CloudStackVersion.parse("99.1.0"); assertNotNull(currentVersion); final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); @@ -234,7 +235,7 @@ public void testCalculateUpgradePathFromKnownDbVersion() { final CloudStackVersion dbVersion = CloudStackVersion.parse("4.17.0.0"); assertNotNull(dbVersion); - final CloudStackVersion currentVersion = CloudStackVersion.parse("4.99.1.0"); + final CloudStackVersion currentVersion = CloudStackVersion.parse("99.1.0"); assertNotNull(currentVersion); final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); @@ -268,10 +269,7 @@ public void testCalculateUpgradePathFromLatestDbVersion() { final CloudStackVersion dbVersion = checker.getLatestVersion(); assertNotNull(dbVersion); - final CloudStackVersion currentVersion = CloudStackVersion.parse(dbVersion.getMajorRelease() + "." - + dbVersion.getMinorRelease() + "." - + dbVersion.getPatchRelease() + "." - + (dbVersion.getSecurityRelease() + 1)); + final CloudStackVersion currentVersion = getNextSecurityRelease(dbVersion); assertNotNull(currentVersion); final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); @@ -293,10 +291,7 @@ public void testCalculateUpgradePathFrom41800toNextSecurityRelease() { final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); assertNotNull(upgrades); - final CloudStackVersion nextSecurityRelease = CloudStackVersion.parse(currentVersion.getMajorRelease() + "." - + currentVersion.getMinorRelease() + "." - + currentVersion.getPatchRelease() + "." - + (currentVersion.getSecurityRelease() + 1)); + final CloudStackVersion nextSecurityRelease = getNextSecurityRelease(currentVersion); assertNotNull(nextSecurityRelease); final DbUpgrade[] upgradesToNext = checker.calculateUpgradePath(dbVersion, nextSecurityRelease); @@ -306,16 +301,26 @@ public void testCalculateUpgradePathFrom41800toNextSecurityRelease() { assertTrue(upgradesToNext[upgradesToNext.length - 1] instanceof NoopDbUpgrade); } + private static CloudStackVersion getNextSecurityRelease(CloudStackVersion version, int increment) { + String nextSecurityReleaseVersionStr = version.getMajorRelease() + "." + + version.getMinorRelease() + "." + + (version.usesNewVersioning() ? "" : version.getPatchRelease() + ".") + + (version.getSecurityRelease() + increment); + + return CloudStackVersion.parse(nextSecurityReleaseVersionStr); + } + + private static CloudStackVersion getNextSecurityRelease(CloudStackVersion version) { + return getNextSecurityRelease(version, 1); + } + @Test public void testCalculateUpgradePathFromSecurityReleaseToLatest() { final CloudStackVersion dbVersion = CloudStackVersion.parse("4.17.2.0"); // a EOL version assertNotNull(dbVersion); - final CloudStackVersion oldSecurityRelease = CloudStackVersion.parse(dbVersion.getMajorRelease() + "." - + dbVersion.getMinorRelease() + "." - + dbVersion.getPatchRelease() + "." - + (dbVersion.getSecurityRelease() + 100)); + final CloudStackVersion oldSecurityRelease = getNextSecurityRelease(dbVersion, 100); assertNotNull(oldSecurityRelease); // fake security release 4.17.2.100 final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); @@ -347,10 +352,7 @@ public void testCalculateUpgradePathFromSecurityReleaseToNextSecurityRelease() { final CloudStackVersion currentVersion = checker.getLatestVersion(); assertNotNull(currentVersion); - final CloudStackVersion nextSecurityRelease = CloudStackVersion.parse(currentVersion.getMajorRelease() + "." - + currentVersion.getMinorRelease() + "." - + currentVersion.getPatchRelease() + "." - + (currentVersion.getSecurityRelease() + 1)); + final CloudStackVersion nextSecurityRelease = getNextSecurityRelease(currentVersion); assertNotNull(nextSecurityRelease); // fake security release final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); From cf0519a683f0e913252ca9ab7fdc7d23435ab734 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 10 Sep 2026 16:04:15 +0530 Subject: [PATCH 04/15] fix imports Signed-off-by: Abhishek Kumar --- .../upgrade/DatabaseUpgradeCheckerTest.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index 763f78fe6949..3810d03161d9 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -16,21 +16,25 @@ // under the License. package com.cloud.upgrade; -import java.sql.SQLException; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.SQLException; import javax.sql.DataSource; import org.apache.cloudstack.utils.CloudStackVersion; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.junit.Test; -import org.junit.Before; import org.junit.After; +import org.junit.Before; +import org.junit.Test; import org.junit.runner.RunWith; - import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.Mockito; @@ -56,15 +60,8 @@ import com.cloud.upgrade.dao.Upgrade471to480; import com.cloud.upgrade.dao.Upgrade480to481; import com.cloud.upgrade.dao.Upgrade490to4910; - import com.cloud.utils.db.TransactionLegacy; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertArrayEquals; - @RunWith(MockitoJUnitRunner.class) public class DatabaseUpgradeCheckerTest { From 044665093c4326eeff4db64c3e5f1ff6c2ee183e Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 10:44:46 +0530 Subject: [PATCH 05/15] fix template version --- .../src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index f3ec76a27c7a..8340d0e84c42 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -517,7 +517,7 @@ protected void doUpgrades(GlobalLock lock) { final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); if (sysVmVersion.usesNewVersioning()) { SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getMinorRelease()); } else { SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); From 1e656d20d0d937148686c0db1616b360497e4ddb Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 16:56:54 +0530 Subject: [PATCH 06/15] continue using 3 part version for sys template --- .../main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 8340d0e84c42..2076c50507bd 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -521,7 +521,8 @@ protected void doUpgrades(GlobalLock lock) { } else { SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); - } + SystemVmTemplateRegistration.CS_MAJOR_VERSION = sysVmVersion.getMajorRelease() + "." + sysVmVersion.getMinorRelease(); + SystemVmTemplateRegistration.CS_TINY_VERSION = sysVmVersion.usesNewVersioning() ? "0" : String.valueOf(sysVmVersion.getPatchRelease()); LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); From 9e0b9b1ead3f8dd5420d82bccf9e37d3d428b05f Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 16:57:53 +0530 Subject: [PATCH 07/15] fix --- .../java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 2076c50507bd..c7f819aee407 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -515,12 +515,8 @@ protected void doUpgrades(GlobalLock lock) { String csVersion = parseSystemVmMetadata(); final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); - if (sysVmVersion.usesNewVersioning()) { - SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.valueOf(sysVmVersion.getMajorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getMinorRelease()); - } else { - SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); + SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); SystemVmTemplateRegistration.CS_MAJOR_VERSION = sysVmVersion.getMajorRelease() + "." + sysVmVersion.getMinorRelease(); SystemVmTemplateRegistration.CS_TINY_VERSION = sysVmVersion.usesNewVersioning() ? "0" : String.valueOf(sysVmVersion.getPatchRelease()); From fca15c82b4710776272b65e6f3bfc8a9caf67304 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 16:58:39 +0530 Subject: [PATCH 08/15] fix more --- .../src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index c7f819aee407..74ee71274cd3 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -516,8 +516,6 @@ protected void doUpgrades(GlobalLock lock) { final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getPatchRelease()); - SystemVmTemplateRegistration.CS_MAJOR_VERSION = sysVmVersion.getMajorRelease() + "." + sysVmVersion.getMinorRelease(); SystemVmTemplateRegistration.CS_TINY_VERSION = sysVmVersion.usesNewVersioning() ? "0" : String.valueOf(sysVmVersion.getPatchRelease()); LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); From 027172a75671d29914893fc7645b321996943ca6 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 17:05:53 +0530 Subject: [PATCH 09/15] use security version after cutover Signed-off-by: Abhishek Kumar --- .../src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 74ee71274cd3..657a3fccdf01 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -516,7 +516,7 @@ protected void doUpgrades(GlobalLock lock) { final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = sysVmVersion.usesNewVersioning() ? "0" : String.valueOf(sysVmVersion.getPatchRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.usesNewVersioning() ? sysVmVersion.getSecurityRelease() : sysVmVersion.getPatchRelease()); LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); From 60b5a2aa76826505669ac71d05d05fd41121f97c Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 15 Sep 2026 18:20:23 +0530 Subject: [PATCH 10/15] improvements and add tests Signed-off-by: Abhishek Kumar --- engine/schema/pom.xml | 14 +++ .../cloud/upgrade/DatabaseUpgradeChecker.java | 15 ++- .../upgrade/DatabaseUpgradeCheckerTest.java | 24 +++++ .../SystemVmTemplateRegistrationTest.java | 37 +++++++ engine/schema/templateConfig.sh | 61 ++++++----- engine/schema/test_templateConfig.sh | 100 ++++++++++++++++++ .../cloudstack/utils/CloudStackVersion.java | 19 +++- .../utils/CloudStackVersionTest.java | 4 +- 8 files changed, 245 insertions(+), 29 deletions(-) create mode 100755 engine/schema/test_templateConfig.sh diff --git a/engine/schema/pom.xml b/engine/schema/pom.xml index a7d53d01b5f7..6e078d7ec9f8 100644 --- a/engine/schema/pom.xml +++ b/engine/schema/pom.xml @@ -153,6 +153,20 @@ exec-maven-plugin ${cs.exec-maven-plugin.version} + + test-templateConfig + test + + exec + + + ${basedir}/ + bash + + test_templateConfig.sh + + + systemvm-template-metadata package diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 657a3fccdf01..334cdd3c7e5f 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -515,8 +515,7 @@ protected void doUpgrades(GlobalLock lock) { String csVersion = parseSystemVmMetadata(); final CloudStackVersion sysVmVersion = CloudStackVersion.parse(csVersion); final CloudStackVersion currentVersion = CloudStackVersion.parse(currentVersionValue); - SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.usesNewVersioning() ? sysVmVersion.getSecurityRelease() : sysVmVersion.getPatchRelease()); + updateSystemVmTemplateVersion(sysVmVersion); LOGGER.info("DB version = {} Code Version = {}", dbVersion, currentVersion); @@ -542,6 +541,18 @@ protected void doUpgrades(GlobalLock lock) { } } + /** + * Sets the system VM template major/tiny version used to look up the matching system VM template, + * from the version parsed out of the system VM template metadata file. Below the versioning cutover + * (major < 24) the tiny version is the legacy patch release; from the cutover onwards it is the + * security release, since the patch position is dropped in that scheme. + */ + @VisibleForTesting + protected static void updateSystemVmTemplateVersion(CloudStackVersion sysVmVersion) { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.usesNewVersioning() ? sysVmVersion.getSecurityRelease() : sysVmVersion.getPatchRelease()); + } + /** * 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. diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index 3810d03161d9..553e537c853f 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -421,4 +421,28 @@ public void testCalculateUpgradePath42010to42100() { assertTrue(upgrades[2] instanceof Upgrade42040to42100); assertEquals(currentVersion.toString(), upgrades[2].getUpgradedVersion()); } + + @Test + public void testUpdateSystemVmTemplateVersionBelowCutover() { + DatabaseUpgradeChecker.updateSystemVmTemplateVersion(CloudStackVersion.parse("4.22.1.0")); + + assertEquals("4.22", SystemVmTemplateRegistration.CS_MAJOR_VERSION); + assertEquals("1", SystemVmTemplateRegistration.CS_TINY_VERSION); + } + + @Test + public void testUpdateSystemVmTemplateVersionAtCutover() { + DatabaseUpgradeChecker.updateSystemVmTemplateVersion(CloudStackVersion.parse("24.0.1")); + + assertEquals("24.0", SystemVmTemplateRegistration.CS_MAJOR_VERSION); + assertEquals("1", SystemVmTemplateRegistration.CS_TINY_VERSION); + } + + @Test + public void testUpdateSystemVmTemplateVersionAfterCutover() { + DatabaseUpgradeChecker.updateSystemVmTemplateVersion(CloudStackVersion.parse("25.3.2")); + + assertEquals("25.3", SystemVmTemplateRegistration.CS_MAJOR_VERSION); + assertEquals("2", SystemVmTemplateRegistration.CS_TINY_VERSION); + } } diff --git a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java index 51db952eb613..dfc67b1dece3 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java @@ -1638,4 +1638,41 @@ public void updateTemplateUrlChecksumAndGuestOsId_ThrowsExceptionWhenUpdateFails assertTrue(exception.getMessage().contains("Exception while updating 'url' and 'checksum' for hypervisor type")); verify(vmTemplateDao).update(templateVO.getId(), templateVO); } + + @Test + public void getSystemVmTemplateVersion_UsesExplicitVersionWhenSet() { + SystemVmTemplateRegistration registration = new SystemVmTemplateRegistration("4.22.0.0"); + + assertEquals("4.22.0.0", registration.getSystemVmTemplateVersion()); + } + + @Test + public void getSystemVmTemplateVersion_BuildsFromMajorAndTinyVersionBelowCutover() { + String previousMajor = SystemVmTemplateRegistration.CS_MAJOR_VERSION; + String previousTiny = SystemVmTemplateRegistration.CS_TINY_VERSION; + try { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = "4.22"; + SystemVmTemplateRegistration.CS_TINY_VERSION = "1"; + + assertEquals("4.22.1", systemVmTemplateRegistration.getSystemVmTemplateVersion()); + } finally { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = previousMajor; + SystemVmTemplateRegistration.CS_TINY_VERSION = previousTiny; + } + } + + @Test + public void getSystemVmTemplateVersion_BuildsFromMajorAndTinyVersionAfterCutover() { + String previousMajor = SystemVmTemplateRegistration.CS_MAJOR_VERSION; + String previousTiny = SystemVmTemplateRegistration.CS_TINY_VERSION; + try { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = "24.0"; + SystemVmTemplateRegistration.CS_TINY_VERSION = "1"; + + assertEquals("24.0.1", systemVmTemplateRegistration.getSystemVmTemplateVersion()); + } finally { + SystemVmTemplateRegistration.CS_MAJOR_VERSION = previousMajor; + SystemVmTemplateRegistration.CS_TINY_VERSION = previousTiny; + } + } } diff --git a/engine/schema/templateConfig.sh b/engine/schema/templateConfig.sh index 21a4ee795745..944e7992d4b5 100755 --- a/engine/schema/templateConfig.sh +++ b/engine/schema/templateConfig.sh @@ -17,16 +17,26 @@ # specific language governing permissions and limitations # under the License. +NEW_VERSIONING_CUTOVER_MAJOR_VERSION=24 + function getTemplateVersion() { projVersion=$1 version="$(cut -d'-' -f1 <<<"$projVersion")" subversion1="$(cut -d'.' -f1 <<<"$version")" subversion2="$(cut -d'.' -f2 <<<"$version")" minorversion="$(cut -d'.' -f3 <<<"$version")" - securityversion="$(cut -d'.' -f4 <<<"$version")" export CS_VERSION="${subversion1}"."${subversion2}" export CS_MINOR_VERSION="${minorversion}" export VERSION="${CS_VERSION}.${CS_MINOR_VERSION}" + if [[ "$subversion1" -ge "$NEW_VERSIONING_CUTOVER_MAJOR_VERSION" ]]; then + # New versioning (major.minor.security): the third component is the + # security release itself, there is no separate patch component. + export FULL_VERSION="${VERSION}" + else + # Legacy versioning (major.minor.patch.security) + securityversion="$(cut -d'.' -f4 <<<"$version")" + export FULL_VERSION="${VERSION}.${securityversion}" + fi export CS_SYSTEMTEMPLATE_REPO="https://download.cloudstack.org/systemvm/" } @@ -64,7 +74,7 @@ function getChecksum() { function createMetadataFile() { local fileData=$(cat "$SOURCEFILE") - echo -e "["default"]\nversion = $VERSION.${securityversion}\ndownloadrepository = $CS_SYSTEMTEMPLATE_REPO\n" >> "$METADATAFILE" + echo -e "["default"]\nversion = $FULL_VERSION\ndownloadrepository = $CS_SYSTEMTEMPLATE_REPO\n" >> "$METADATAFILE" for template in "${templates[@]}" do section="${template%%:*}" @@ -81,27 +91,30 @@ function createMetadataFile() { done } -declare -a templates -getTemplateVersion $1 -declare -A template_specs=( - [kvm-x86_64]="x86_64-kvm.qcow2.bz2" - [kvm-aarch64]="aarch64-kvm.qcow2.bz2" - [vmware]="x86_64-vmware.ova" - [xenserver]="x86_64-xen.vhd.bz2" - [hyperv]="x86_64-hyperv.vhd.zip" - [lxc]="x86_64-kvm.qcow2.bz2" - [ovm3]="x86_64-ovm.raw.bz2" -) +# Guard so the file can be sourced (e.g. by tests) without running the steps below. +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + declare -a templates + getTemplateVersion $1 + declare -A template_specs=( + [kvm-x86_64]="x86_64-kvm.qcow2.bz2" + [kvm-aarch64]="aarch64-kvm.qcow2.bz2" + [vmware]="x86_64-vmware.ova" + [xenserver]="x86_64-xen.vhd.bz2" + [hyperv]="x86_64-hyperv.vhd.zip" + [lxc]="x86_64-kvm.qcow2.bz2" + [ovm3]="x86_64-ovm.raw.bz2" + ) -templates=() -for key in "${!template_specs[@]}"; do - url="${CS_SYSTEMTEMPLATE_REPO}/${CS_VERSION}/systemvmtemplate-$VERSION-${template_specs[$key]}" - templates+=("$key:$url") -done + templates=() + for key in "${!template_specs[@]}"; do + url="${CS_SYSTEMTEMPLATE_REPO}/${CS_VERSION}/systemvmtemplate-$VERSION-${template_specs[$key]}" + templates+=("$key:$url") + done -PARENTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/dist/systemvm-templates/" -mkdir -p "$PARENTPATH" -METADATAFILE="${PARENTPATH}metadata.ini" -echo > "$METADATAFILE" -SOURCEFILE="${PARENTPATH}sha512sum.txt" -createMetadataFile + PARENTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/dist/systemvm-templates/" + mkdir -p "$PARENTPATH" + METADATAFILE="${PARENTPATH}metadata.ini" + echo > "$METADATAFILE" + SOURCEFILE="${PARENTPATH}sha512sum.txt" + createMetadataFile +fi diff --git a/engine/schema/test_templateConfig.sh b/engine/schema/test_templateConfig.sh new file mode 100755 index 000000000000..2a29268bacd5 --- /dev/null +++ b/engine/schema/test_templateConfig.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# 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. +# +# Standalone smoke test for templateConfig.sh's version handling across the +# 4.x (legacy) to 24.x (cutover) versioning schemes. Run directly: +# bash engine/schema/test_templateConfig.sh + +set -u + +SCRIPT_DIR="$( cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 ; pwd -P )" +# shellcheck source=templateConfig.sh +source "${SCRIPT_DIR}/templateConfig.sh" + +pass=0 +fail=0 + +assert_eq() { + local description="$1" + local expected="$2" + local actual="$3" + if [[ "$expected" == "$actual" ]]; then + pass=$((pass + 1)) + echo "PASS: $description" + else + fail=$((fail + 1)) + echo "FAIL: $description (expected '$expected', got '$actual')" + fi +} + +# --- getTemplateVersion: legacy versioning (major < 24) --- + +getTemplateVersion "4.22.0.0" +assert_eq "legacy: CS_VERSION" "4.22" "$CS_VERSION" +assert_eq "legacy: VERSION" "4.22.0" "$VERSION" +assert_eq "legacy: FULL_VERSION keeps the 4th (security) component" "4.22.0.0" "$FULL_VERSION" + +getTemplateVersion "4.23.0.1-SNAPSHOT" +assert_eq "legacy with -SNAPSHOT suffix: VERSION" "4.23.0" "$VERSION" +assert_eq "legacy with -SNAPSHOT suffix: FULL_VERSION" "4.23.0.1" "$FULL_VERSION" + +# --- getTemplateVersion: new versioning (major >= 24, post-cutover) --- + +getTemplateVersion "24.0.0" +assert_eq "cutover: CS_VERSION" "24.0" "$CS_VERSION" +assert_eq "cutover: VERSION" "24.0.0" "$VERSION" +assert_eq "cutover: FULL_VERSION has no trailing dot (patch dropped)" "24.0.0" "$FULL_VERSION" + +getTemplateVersion "24.1.2-SNAPSHOT" +assert_eq "cutover with -SNAPSHOT suffix: VERSION" "24.1.2" "$VERSION" +assert_eq "cutover with -SNAPSHOT suffix: FULL_VERSION" "24.1.2" "$FULL_VERSION" + +getTemplateVersion "99.9.9" +assert_eq "future major: VERSION" "99.9.9" "$VERSION" +assert_eq "future major: FULL_VERSION" "99.9.9" "$FULL_VERSION" + +# --- createMetadataFile: end-to-end metadata.ini "version" line --- + +run_create_metadata_file() { + local projVersion="$1" + local workdir + workdir="$(mktemp -d)" + + getTemplateVersion "$projVersion" + METADATAFILE="${workdir}/metadata.ini" + SOURCEFILE="${workdir}/sha512sum.txt" + printf "abc123 systemvmtemplate-%s-x86_64-kvm.qcow2.bz2\n" "$VERSION" > "$SOURCEFILE" + templates=("kvm-x86_64:${CS_SYSTEMTEMPLATE_REPO}/${CS_VERSION}/systemvmtemplate-${VERSION}-x86_64-kvm.qcow2.bz2") + : > "$METADATAFILE" + + createMetadataFile + grep '^version = ' "$METADATAFILE" | head -1 | cut -d' ' -f3 + + rm -rf "$workdir" +} + +actual="$(run_create_metadata_file "4.22.0.0")" +assert_eq "metadata.ini legacy 'version' line" "4.22.0.0" "$actual" + +actual="$(run_create_metadata_file "24.0.0")" +assert_eq "metadata.ini cutover 'version' line has no trailing dot" "24.0.0" "$actual" + +echo "" +echo "${pass} passed, ${fail} failed" +[[ $fail -eq 0 ]] diff --git a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java index 8eb4c6ab9289..7e8bc9126d96 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java @@ -75,7 +75,9 @@ private CloudStackVersion(final int majorRelease, final int minorRelease, final *
  • <major>.<minor>.<security release> (for versions >= 24.0.0)
  • * * - * Legacy patch-based formats remain supported for backward compatibility. + * Legacy patch-based formats remain supported for backward compatibility, e.g. 4.23.0.1-SNAPSHOT, + * but only below major version 24: a 4-position value whose major release is at or above 24, e.g. + * 24.0.0.1, is rejected. * * If the string contains a suffix that begins with a "-" character, then the "-" and all characters following it * will be dropped. @@ -208,13 +210,26 @@ public int compareTo(final CloudStackVersion thatVersion) { public static String trimRouterVersion(String version) { final String[] tokens = version.split(" "); - if (tokens.length >= 3 && FULL_VERSION_FORMAT.matcher(tokens[2]).matches()) { + if (tokens.length >= 3 && FULL_VERSION_FORMAT.matcher(tokens[2]).matches() && isParseableVersion(tokens[2])) { return tokens[2]; } return "0"; } + /** + * Whether {@link #parse(String)} would accept the given value, following the same cutover rule that + * rejects a 4-position major.minor.patch.security value once the major release reaches 24. + */ + private static boolean isParseableVersion(final String value) { + try { + parse(value); + return true; + } catch (RuntimeException e) { + return false; + } + } + private static ImmutableList normalizeVersionValues(final ImmutableList values) { checkArgument(values != null); diff --git a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java index 4d0b4cb0439b..4892644e8688 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java @@ -231,7 +231,9 @@ public void testGreaterThanAndLessThanCompareDirect(final String value, final St "Cloudstack Release 1.2.3.4-brnading-1519453362 Mon Jan 1 10:10:10 UTC 2018, 1.2.3.4-brnading-1519453362", "Cloudstack Release 1.2 Mon Jan 1 10:10:10 UTC 2018, 0", "Cloudstack Release 1.2-SNAPSHOT Mon Jan 1 10:10:10 UTC 2018, 0", - "Cloud stack Release 1.2.3.4 Mon Jan 1 10:10:10 UTC 2018, 0" + "Cloud stack Release 1.2.3.4 Mon Jan 1 10:10:10 UTC 2018, 0", + "Cloudstack Release 24.0.0 Mon Jan 1 10:10:10 UTC 2018, 24.0.0", + "Cloudstack Release 24.0.0.1 Mon Jan 1 10:10:10 UTC 2018, 0" }) public void testTrimRouterVersion(final String value, final String expected) { From 1754a6401bc47974ab29bfe02f22fedba07d69cc Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Sep 2026 06:58:00 +0530 Subject: [PATCH 11/15] fix more scripts Signed-off-by: Abhishek Kumar --- scripts/installer/export-templates.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/installer/export-templates.sh b/scripts/installer/export-templates.sh index dbd560e44c76..75da49a7653e 100755 --- a/scripts/installer/export-templates.sh +++ b/scripts/installer/export-templates.sh @@ -16,11 +16,21 @@ # specific language governing permissions and limitations # under the License. +NEW_VERSIONING_CUTOVER_MAJOR_VERSION=24 + METADATA_FILE="metadata.ini" IMAGE_PATH=${3:-"/usr/share/cloudstack-management/templates/systemvm/"} TEMPLATE_VERSION=$(awk -F "=" '/version/ {print $2}' ${IMAGE_PATH}${METADATA_FILE} | xargs) TEMPLATE_PATH="/usr/share/cloudstack-management/templates/systemvm/" -VERSION="${TEMPLATE_VERSION%.*}" +TEMPLATE_MAJOR_VERSION="$(cut -d'.' -f1 <<<"$TEMPLATE_VERSION")" +if [[ "$TEMPLATE_MAJOR_VERSION" -ge "$NEW_VERSIONING_CUTOVER_MAJOR_VERSION" ]]; then + # New versioning (major.minor.security): the generated template files use the full version as-is. + VERSION="${TEMPLATE_VERSION}" +else + # Legacy versioning (major.minor.patch.security): the generated template files drop the trailing + # security component. + VERSION="${TEMPLATE_VERSION%.*}" +fi PREFIX=${4:-"systemvmtemplate-$VERSION"} CLEANUP=${2:-1} TEMP_IMAGE_PATH="/tmp/sysvm_convert/" From 539ae853ccf8b24272f9cbb25375a6a7565a47a6 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Sep 2026 07:20:26 +0530 Subject: [PATCH 12/15] address remaining comments Signed-off-by: Abhishek Kumar --- .../upgrade/DatabaseUpgradeCheckerTest.java | 29 ++++++++++++++++ .../cloudstack/veeam/api/dto/VersionTest.java | 34 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index 553e537c853f..aa39567af842 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -52,6 +52,7 @@ import com.cloud.upgrade.dao.Upgrade42020to42030; import com.cloud.upgrade.dao.Upgrade42030to42040; import com.cloud.upgrade.dao.Upgrade42040to42100; +import com.cloud.upgrade.dao.Upgrade42300to2400; import com.cloud.upgrade.dao.Upgrade452to453; import com.cloud.upgrade.dao.Upgrade453to460; import com.cloud.upgrade.dao.Upgrade460to461; @@ -79,6 +80,8 @@ public class DatabaseUpgradeCheckerTest { ResultSet resultSet; private DataSource backupDataSource; + private String previousCsMajorVersion; + private String previousCsTinyVersion; @Before public void setup() throws Exception { @@ -90,6 +93,9 @@ public void setup() throws Exception { Mockito.when(dataSource.getConnection()).thenReturn(connection); Mockito.when(connection.prepareStatement(ArgumentMatchers.anyString())).thenReturn(preparedStatement); Mockito.when(preparedStatement.executeQuery()).thenReturn(resultSet); + + previousCsMajorVersion = SystemVmTemplateRegistration.CS_MAJOR_VERSION; + previousCsTinyVersion = SystemVmTemplateRegistration.CS_TINY_VERSION; } @After @@ -97,6 +103,9 @@ public void cleanup() throws Exception { Field dsField = TransactionLegacy.class.getDeclaredField("s_ds"); dsField.setAccessible(true); dsField.set(null, backupDataSource); + + SystemVmTemplateRegistration.CS_MAJOR_VERSION = previousCsMajorVersion; + SystemVmTemplateRegistration.CS_TINY_VERSION = previousCsTinyVersion; } @Test @@ -422,6 +431,26 @@ public void testCalculateUpgradePath42010to42100() { assertEquals(currentVersion.toString(), upgrades[2].getUpgradedVersion()); } + @Test + public void testCalculateUpgradePath42300to2400() { + + final CloudStackVersion dbVersion = CloudStackVersion.parse("4.23.0.0"); + assertNotNull(dbVersion); + + final CloudStackVersion currentVersion = CloudStackVersion.parse("24.0.0"); + assertNotNull(currentVersion); + + final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); + final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); + + assertNotNull(upgrades); + assertEquals(1, upgrades.length); + assertTrue(upgrades[0] instanceof Upgrade42300to2400); + + assertArrayEquals(new String[]{"4.23.0.0", "24.0.0"}, upgrades[0].getUpgradableVersionRange()); + assertEquals(currentVersion.toString(), upgrades[0].getUpgradedVersion()); + } + @Test public void testUpdateSystemVmTemplateVersionBelowCutover() { DatabaseUpgradeChecker.updateSystemVmTemplateVersion(CloudStackVersion.parse("4.22.1.0")); diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/VersionTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/VersionTest.java index a91f193ba42a..72f1d92755e9 100644 --- a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/VersionTest.java +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/dto/VersionTest.java @@ -62,6 +62,40 @@ public void fromPackageAndCSVersion_IncompleteVersion_DoesNotSetFullVersion() { } } + @Test + public void fromPackageAndCSVersion_NewVersioningZeroSecurity_MapsSecurityToBuildAndOmitsRevision() { + CloudStackVersion csVersion = CloudStackVersion.parse("24.0.0"); + try (MockedStatic mocked = Mockito.mockStatic(VeeamControlService.class)) { + mocked.when(VeeamControlService::getPackageVersion).thenReturn("24.0.0"); + mocked.when(VeeamControlService::getCSVersion).thenReturn(csVersion); + + Version version = Version.fromPackageAndCSVersion(true); + + assertEquals("24.0.0", version.getFullVersion()); + assertEquals("24", version.getMajor()); + assertEquals("0", version.getMinor()); + assertEquals("0", version.getBuild()); + assertNull(version.getRevision()); + } + } + + @Test + public void fromPackageAndCSVersion_NewVersioningNonZeroSecurity_MapsSecurityToBuildAndOmitsRevision() { + CloudStackVersion csVersion = CloudStackVersion.parse("24.1.3"); + try (MockedStatic mocked = Mockito.mockStatic(VeeamControlService.class)) { + mocked.when(VeeamControlService::getPackageVersion).thenReturn("24.1.3"); + mocked.when(VeeamControlService::getCSVersion).thenReturn(csVersion); + + Version version = Version.fromPackageAndCSVersion(true); + + assertEquals("24.1.3", version.getFullVersion()); + assertEquals("24", version.getMajor()); + assertEquals("1", version.getMinor()); + assertEquals("3", version.getBuild()); + assertNull(version.getRevision()); + } + } + @Test public void fromPackageAndCSVersion_NullCloudStackVersion_ReturnsWithoutNumericParts() { try (MockedStatic mocked = Mockito.mockStatic(VeeamControlService.class)) { From 54b1fe4ad99d5ea089fc769f7c93956b1e8d62ff Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Sep 2026 09:56:22 +0530 Subject: [PATCH 13/15] more changes Signed-off-by: Abhishek Kumar --- .../cloud/upgrade/DatabaseUpgradeChecker.java | 2 +- engine/schema/templateConfig.sh | 22 ++++- engine/schema/test_templateConfig.sh | 12 +++ .../cloudstack/veeam/api/dto/Version.java | 6 +- scripts/installer/export-templates.sh | 2 + .../nvp/plugin/NiciraNvpApiVersion.java | 2 +- .../cloudstack/utils/CloudStackVersion.java | 87 ++++++++++++++++--- .../utils/CloudStackVersionTest.java | 75 ++++++++++++++-- 8 files changed, 181 insertions(+), 27 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 334cdd3c7e5f..57ae2c3d6491 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -550,7 +550,7 @@ protected void doUpgrades(GlobalLock lock) { @VisibleForTesting protected static void updateSystemVmTemplateVersion(CloudStackVersion sysVmVersion) { SystemVmTemplateRegistration.CS_MAJOR_VERSION = String.format("%d.%d", sysVmVersion.getMajorRelease(), sysVmVersion.getMinorRelease()); - SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.usesNewVersioning() ? sysVmVersion.getSecurityRelease() : sysVmVersion.getPatchRelease()); + SystemVmTemplateRegistration.CS_TINY_VERSION = String.valueOf(sysVmVersion.getTinyRelease()); } /** diff --git a/engine/schema/templateConfig.sh b/engine/schema/templateConfig.sh index 944e7992d4b5..7a8283e77c1c 100755 --- a/engine/schema/templateConfig.sh +++ b/engine/schema/templateConfig.sh @@ -17,6 +17,8 @@ # specific language governing permissions and limitations # under the License. +# Kept in sync with CloudStackVersion.NEW_VERSIONING_CUTOVER_MAJOR_VERSION (utils module) and the +# same constant in scripts/installer/export-templates.sh. NEW_VERSIONING_CUTOVER_MAJOR_VERSION=24 function getTemplateVersion() { @@ -25,17 +27,29 @@ function getTemplateVersion() { subversion1="$(cut -d'.' -f1 <<<"$version")" subversion2="$(cut -d'.' -f2 <<<"$version")" minorversion="$(cut -d'.' -f3 <<<"$version")" + fourthversion="$(cut -d'.' -f4 <<<"$version")" export CS_VERSION="${subversion1}"."${subversion2}" export CS_MINOR_VERSION="${minorversion}" export VERSION="${CS_VERSION}.${CS_MINOR_VERSION}" if [[ "$subversion1" -ge "$NEW_VERSIONING_CUTOVER_MAJOR_VERSION" ]]; then # New versioning (major.minor.security): the third component is the - # security release itself, there is no separate patch component. + # security release itself, there is no separate patch component. A + # 4th component is invalid in this scheme (matches CloudStackVersion.parse()) + # and must not be silently dropped. + if [[ -n "$fourthversion" ]]; then + echo "Invalid version '${projVersion}': major versions at or above ${NEW_VERSIONING_CUTOVER_MAJOR_VERSION} do not support a 4-component major.minor.patch.security format" >&2 + exit 1 + fi export FULL_VERSION="${VERSION}" else - # Legacy versioning (major.minor.patch.security) - securityversion="$(cut -d'.' -f4 <<<"$version")" - export FULL_VERSION="${VERSION}.${securityversion}" + # Legacy versioning: major.minor.patch, or major.minor.patch.security when a 4th + # component is present. Without a 4th component, VERSION is already correct as-is; + # appending an empty security component would leave a dangling trailing dot. + if [[ -n "$fourthversion" ]]; then + export FULL_VERSION="${VERSION}.${fourthversion}" + else + export FULL_VERSION="${VERSION}" + fi fi export CS_SYSTEMTEMPLATE_REPO="https://download.cloudstack.org/systemvm/" } diff --git a/engine/schema/test_templateConfig.sh b/engine/schema/test_templateConfig.sh index 2a29268bacd5..2acb4281edb7 100755 --- a/engine/schema/test_templateConfig.sh +++ b/engine/schema/test_templateConfig.sh @@ -54,6 +54,11 @@ getTemplateVersion "4.23.0.1-SNAPSHOT" assert_eq "legacy with -SNAPSHOT suffix: VERSION" "4.23.0" "$VERSION" assert_eq "legacy with -SNAPSHOT suffix: FULL_VERSION" "4.23.0.1" "$FULL_VERSION" +# A legacy 3-component major.minor.patch value (no security component) is a documented, valid +# CloudStackVersion.parse() format below the cutover and must not get a dangling trailing dot. +getTemplateVersion "4.23.1" +assert_eq "legacy 3-component (no security): FULL_VERSION has no trailing dot" "4.23.1" "$FULL_VERSION" + # --- getTemplateVersion: new versioning (major >= 24, post-cutover) --- getTemplateVersion "24.0.0" @@ -69,6 +74,13 @@ getTemplateVersion "99.9.9" assert_eq "future major: VERSION" "99.9.9" "$VERSION" assert_eq "future major: FULL_VERSION" "99.9.9" "$FULL_VERSION" +# A 4-component major.minor.patch.security value is invalid at/after the cutover (matches +# CloudStackVersion.parse()) and must fail the build rather than silently drop the 4th component. +# Run in a subshell since getTemplateVersion calls `exit` on this input. +( getTemplateVersion "24.0.0.1" ) >/dev/null 2>&1 +cutover_4component_exit_code="$?" +assert_eq "cutover: 4-component version is rejected instead of silently normalized" "1" "$cutover_4component_exit_code" + # --- createMetadataFile: end-to-end metadata.ini "version" line --- run_create_metadata_file() { diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java index 2d14443e2d58..5e1aa52533c1 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java @@ -87,10 +87,8 @@ public static Version fromPackageAndCSVersion(boolean complete) { } version.setMajor(String.valueOf(csVersion.getMajorRelease())); version.setMinor(String.valueOf(csVersion.getMinorRelease())); - if (csVersion.usesNewVersioning()) { - version.setBuild(String.valueOf(csVersion.getSecurityRelease())); - } else { - version.setBuild(String.valueOf(csVersion.getPatchRelease())); + version.setBuild(String.valueOf(csVersion.getTinyRelease())); + if (!csVersion.usesNewVersioning()) { version.setRevision(String.valueOf(csVersion.getSecurityRelease())); } return version; diff --git a/scripts/installer/export-templates.sh b/scripts/installer/export-templates.sh index 75da49a7653e..cd44ed674e5e 100755 --- a/scripts/installer/export-templates.sh +++ b/scripts/installer/export-templates.sh @@ -16,6 +16,8 @@ # specific language governing permissions and limitations # under the License. +# Kept in sync with CloudStackVersion.NEW_VERSIONING_CUTOVER_MAJOR_VERSION (utils module) and the +# same constant in engine/schema/templateConfig.sh. NEW_VERSIONING_CUTOVER_MAJOR_VERSION=24 METADATA_FILE="metadata.ini" diff --git a/utils/src/main/java/com/cloud/utils/nicira/nvp/plugin/NiciraNvpApiVersion.java b/utils/src/main/java/com/cloud/utils/nicira/nvp/plugin/NiciraNvpApiVersion.java index 7e4dd5b19b4e..5c79d78a52a9 100755 --- a/utils/src/main/java/com/cloud/utils/nicira/nvp/plugin/NiciraNvpApiVersion.java +++ b/utils/src/main/java/com/cloud/utils/nicira/nvp/plugin/NiciraNvpApiVersion.java @@ -37,7 +37,7 @@ public static synchronized boolean isApiVersionLowerThan(String apiVersion){ if (niciraApiVersion == null) { return false; } - int compare = CloudStackVersion.compare(niciraApiVersion, apiVersion); + int compare = CloudStackVersion.compare(niciraApiVersion, apiVersion, true); return (compare < 0); } diff --git a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java index 7e8bc9126d96..fd15df7d5ea8 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java @@ -41,20 +41,30 @@ public final class CloudStackVersion implements Comparable { private final static Pattern NUMBER_VERSION_FORMAT = Pattern.compile("\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?"); private final static Pattern FULL_VERSION_FORMAT = Pattern.compile("\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?(?:-[a-zA-Z]+)?(?:-\\d+)?(?:-SNAPSHOT)?"); + // Kept in sync with the same constant in engine/schema/templateConfig.sh and + // scripts/installer/export-templates.sh, which independently generate/consume + // system VM template metadata using this same versioning cutover rule. private final static int NEW_VERSIONING_CUTOVER_MAJOR_VERSION = 24; private final int majorRelease; private final int minorRelease; - private final Integer patchRelease; + private final int patchRelease; private final Integer securityRelease; + // Whether this instance was parsed via parse(value, true) — a version scheme other than + // CloudStack's own. Deliberately excluded from equals()/hashCode(): two instances with the same + // component values represent the same version regardless of which parsing mode produced them. + // It only gates whether the new-versioning cutover rule's *interpretation* (toString()'s + // canonicalization, usesNewVersioning()) applies, since that rule is specific to CloudStack's own + // release numbering. + private final boolean external; - private CloudStackVersion(final int majorRelease, final int minorRelease, final Integer patchRelease, final Integer securityRelease) { + private CloudStackVersion(final int majorRelease, final int minorRelease, final int patchRelease, final Integer securityRelease, final boolean external) { super(); checkArgument(majorRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a majorRelease greater than 0."); checkArgument(minorRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a minorRelease greater than 0."); - checkArgument(patchRelease == null || patchRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a patchRelease greater than 0."); + checkArgument(patchRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a patchRelease greater than 0."); checkArgument(securityRelease == null || securityRelease >= 0, CloudStackVersion.class.getName() + "(int, int, int, Integer) requires a null securityRelease or a non-null value greater than 0."); @@ -62,6 +72,7 @@ private CloudStackVersion(final int majorRelease, final int minorRelease, final this.minorRelease = minorRelease; this.patchRelease = patchRelease; this.securityRelease = securityRelease; + this.external = external; } @@ -90,6 +101,26 @@ private CloudStackVersion(final int majorRelease, final int minorRelease, final * */ public static CloudStackVersion parse(final String value) { + return parse(value, false); + } + + /** + * Parses a version string the same way as {@link #parse(String)}, but with {@code external} set to + * true, always applies the legacy major.minor.patch(.security) component mapping and + * never the major-24-and-above new-versioning cutover rule. + *

    + * CloudStack's own versioning cutover (see {@link #NEW_VERSIONING_CUTOVER_MAJOR_VERSION}) is a fact + * about CloudStack's own release numbering. It has no bearing on unrelated version schemes, such as a + * VMware/ESXi hypervisor version, that may coincidentally reach the same major version number. Callers + * parsing such external version strings must pass external = true so a value like + * 24.0.1 is not misread as a CloudStack security release. + * + * @param value The value to parse which must be non-blank and conform the formats listed above + * @param external whether {@code value} comes from a version scheme other than CloudStack's own + * + * @return value parsed into a CloudStackVersion instance + */ + public static CloudStackVersion parse(final String value, final boolean external) { // Strip out any legacy patch information from the version string ... final String trimmedValue = StringUtils.substringBefore(value, "-"); @@ -111,12 +142,12 @@ public static CloudStackVersion parse(final String value) { final Integer securityRelease; if (components.length == 4) { - checkArgument(isLegacyVersioning(majorRelease), CloudStackVersion.class.getName() + ".parse(String) passed " + value + + checkArgument(external || isLegacyVersioning(majorRelease), CloudStackVersion.class.getName() + ".parse(String) passed " + value + ", but major versions at or above 24 do not support legacy int.int.int.int format"); // Deprecated legacy format: major.minor.patch.security patchRelease = thirdComponent; securityRelease = Integer.valueOf(components[3]); - } else if (isNewVersioning(majorRelease)) { + } else if (!external && isNewVersioning(majorRelease)) { // New format: major.minor.securityRelease (patch dropped) patchRelease = 0; securityRelease = thirdComponent; @@ -126,7 +157,7 @@ public static CloudStackVersion parse(final String value) { securityRelease = null; } - return new CloudStackVersion(majorRelease, minorRelease, patchRelease, securityRelease); + return new CloudStackVersion(majorRelease, minorRelease, patchRelease, securityRelease, external); } @@ -143,7 +174,26 @@ public static CloudStackVersion parse(final String value) { * @since 4.12.0.0 */ public static int compare(String version1, String version2) { - return parse(version1).compareTo(parse(version2)); + return compare(version1, version2, false); + } + + /** + * Shortcut method to {@link #parse(String, boolean)} and {@link #compareTo(CloudStackVersion)} two versions. + * Pass external = true when comparing version strings from a scheme other than CloudStack's + * own (e.g. a VMware/ESXi or NSX/Nicira NVP version), so CloudStack's own new-versioning cutover rule is + * not applied to them. + * + * @param version1 the first value to be parsed and compared + * @param version2 the second value to be parsed and compared + * @param external whether version1/version2 come from a version scheme other + * than CloudStack's own + * + * @return A value less than zero (0) indicates version1 is less than version2. A value + * equal to zero (0) indicates version1 equals version2. A value greater than zero (0) + * indicates version1 is greater than version2. + */ + public static int compare(String version1, String version2, boolean external) { + return parse(version1, external).compareTo(parse(version2, external)); } /** @@ -214,7 +264,11 @@ public static String trimRouterVersion(String version) { return tokens[2]; } - return "0"; + // A sentinel that sorts lower than any real version, so callers that feed this straight into + // parse()/compare() (as the router-version-check call sites do, with no try/catch) get a + // "definitely needs upgrading" result instead of an uncaught parse failure. Must itself be a + // value parse() accepts. + return "0.0.0"; } /** @@ -289,7 +343,18 @@ public Integer getSecurityRelease() { } public boolean usesNewVersioning() { - return isNewVersioning(majorRelease); + return !external && isNewVersioning(majorRelease); + } + + /** + * The release component that identifies this version's "tiny"/build release: the security + * release for a new-versioning (post-cutover) CloudStack version, or the patch release otherwise. + * Consolidates a mapping that callers (e.g. system VM template version resolution, the Veeam + * integration's version reporting) would otherwise have to re-derive from + * {@link #usesNewVersioning()} themselves. + */ + public int getTinyRelease() { + return usesNewVersioning() ? securityRelease : patchRelease; } @Override @@ -319,7 +384,7 @@ public int hashCode() { @Override public String toString() { // Canonicalize cutover-and-later versions to major.minor.securityRelease. - if (securityRelease != null && patchRelease == 0 && isNewVersioning(majorRelease)) { + if (securityRelease != null && patchRelease == 0 && usesNewVersioning()) { return Joiner.on(".").join(ImmutableList.of(majorRelease, minorRelease, securityRelease)); } @@ -332,7 +397,7 @@ public String toString() { */ public static String getVMwareParentVersion(String hypervisorVersion) { try { - CloudStackVersion version = CloudStackVersion.parse(hypervisorVersion); + CloudStackVersion version = CloudStackVersion.parse(hypervisorVersion, true); String parentVersion = String.format("%s.%s", version.getMajorRelease(), version.getMinorRelease()); if (version.getPatchRelease() != 0) { parentVersion = String.format("%s.%s", parentVersion, version.getPatchRelease()); diff --git a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java index 4892644e8688..e9bbb35af3dc 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/CloudStackVersionTest.java @@ -87,12 +87,58 @@ public void testInvalidParse(final String invalidValue) { CloudStackVersion.parse(invalidValue); } + @Test + public void testExternalParseIgnoresCutoverRule() { + // A 3-component value with a major at/above the cutover is treated as major.minor.patch, + // not major.minor.security, when parsed as an external (non-CloudStack) version. + CloudStackVersion version = CloudStackVersion.parse("24.1.1", true); + assertEquals(24, version.getMajorRelease()); + assertEquals(1, version.getMinorRelease()); + assertEquals(1, version.getPatchRelease()); + Assert.assertNull(version.getSecurityRelease()); + + // A 4-component value with a major at/above the cutover, rejected by parse(value), is accepted + // when parsed as external. + CloudStackVersion legacyShapedVersion = CloudStackVersion.parse("24.1.1.2", true); + assertEquals(24, legacyShapedVersion.getMajorRelease()); + assertEquals(1, legacyShapedVersion.getMinorRelease()); + assertEquals(1, legacyShapedVersion.getPatchRelease()); + assertEquals(Integer.valueOf(2), legacyShapedVersion.getSecurityRelease()); + } + + @Test + public void testExternalParseNotCanonicalizedAsCloudStackVersion() { + // An external version with major >= 24 and a literal patch digit of 0 (e.g. a 4-component + // value like "24.0.0.5") must not have its toString()/usesNewVersioning() apply CloudStack's + // own cutover canonicalization: doing so would misreport it as CloudStack version "24.0.5", + // silently discarding the real ("0") patch component. + CloudStackVersion version = CloudStackVersion.parse("24.0.0.5", true); + Assert.assertFalse(version.usesNewVersioning()); + assertEquals("24.0.0.5", version.toString()); + } + + @Test + public void testExternalCompareIgnoresCutoverRule() { + // Without external=true, "24.1.0" would be new-versioning (patch dropped to 0, third + // component treated as security), making it equal to "24.1.1"; external=true keeps the + // patch component significant so these two compare as different, non-CloudStack versions. + assertEquals(0, CloudStackVersion.compare("24.1.0", "24.1.0", true)); + Assert.assertTrue(CloudStackVersion.compare("24.1.1", "24.1.0", true) > 0); + Assert.assertTrue(CloudStackVersion.compare("24.1.0", "24.1.1", true) < 0); + + // A 4-component value with a major at/above the cutover, rejected by compare(v1, v2), is + // accepted when compared as external. + assertEquals(0, CloudStackVersion.compare("24.1.1.2", "24.1.1.2", true)); + } + @Test @DataProvider({ "1.0.0", "1.0.0.0", "1.2.3", - "1.2.3.4" + "1.2.3.4", + "1.2.200", + "1.2.200.5" }) public void testEquals(final String value) { @@ -229,11 +275,11 @@ public void testGreaterThanAndLessThanCompareDirect(final String value, final St "Cloudstack Release 1.2.3.4-1519453362 Mon Jan 1 10:10:10 UTC 2018, 1.2.3.4-1519453362", "Cloudstack Release 1.2.3.4-brnading-SNAPSHOT Mon Jan 1 10:10:10 UTC 2018, 1.2.3.4-brnading-SNAPSHOT", "Cloudstack Release 1.2.3.4-brnading-1519453362 Mon Jan 1 10:10:10 UTC 2018, 1.2.3.4-brnading-1519453362", - "Cloudstack Release 1.2 Mon Jan 1 10:10:10 UTC 2018, 0", - "Cloudstack Release 1.2-SNAPSHOT Mon Jan 1 10:10:10 UTC 2018, 0", - "Cloud stack Release 1.2.3.4 Mon Jan 1 10:10:10 UTC 2018, 0", + "Cloudstack Release 1.2 Mon Jan 1 10:10:10 UTC 2018, 0.0.0", + "Cloudstack Release 1.2-SNAPSHOT Mon Jan 1 10:10:10 UTC 2018, 0.0.0", + "Cloud stack Release 1.2.3.4 Mon Jan 1 10:10:10 UTC 2018, 0.0.0", "Cloudstack Release 24.0.0 Mon Jan 1 10:10:10 UTC 2018, 24.0.0", - "Cloudstack Release 24.0.0.1 Mon Jan 1 10:10:10 UTC 2018, 0" + "Cloudstack Release 24.0.0.1 Mon Jan 1 10:10:10 UTC 2018, 0.0.0" }) public void testTrimRouterVersion(final String value, final String expected) { @@ -241,6 +287,18 @@ public void testTrimRouterVersion(final String value, final String expected) { } + @Test + public void testTrimRouterVersionInvalidFallbackIsItselfParseable() { + // The invalid-version fallback must be a value CloudStackVersion.parse()/compare() accepts: + // callers (e.g. NetworkHelperImpl.checkRouterVersion) pass it straight into compare() with no + // try/catch, so an unparseable fallback would turn a "router needs upgrading" case into an + // uncaught exception. + String fallback = CloudStackVersion.trimRouterVersion("not a router version string"); + CloudStackVersion parsed = CloudStackVersion.parse(fallback); + assertNotNull(parsed); + Assert.assertTrue(CloudStackVersion.compare(fallback, "1.0.0") < 0); + } + private void verifyGetVMwareParentVersion(String hypervisorVersion, String expectedParentVersion) { if (expectedParentVersion == null) { Assert.assertNull(CloudStackVersion.getVMwareParentVersion(hypervisorVersion)); @@ -259,6 +317,11 @@ public void testGetParentVersion() { verifyGetVMwareParentVersion("8.0.0", "8.0"); verifyGetVMwareParentVersion("8.0.0.2", "8.0"); verifyGetVMwareParentVersion("8.0.1.0", "8.0.1"); - verifyGetVMwareParentVersion("24.1.1", "24.1"); + // A hypervisor version whose major happens to match CloudStack's own new-versioning cutover (24) + // must still be treated as an ordinary major.minor.patch VMware version, not as a CloudStack + // major.minor.security value: the patch component ("1") must not be dropped. + verifyGetVMwareParentVersion("24.1.0", "24.1"); + verifyGetVMwareParentVersion("24.1.1", "24.1.1"); + verifyGetVMwareParentVersion("24.1.1.2", "24.1.1"); } } From 8b399d6907548804eda8919fd81469d9e41588c3 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 16 Sep 2026 14:44:18 +0530 Subject: [PATCH 14/15] Update versioning logic in export-templates.sh Refactor versioning logic to handle four-component metadata separately. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/installer/export-templates.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/installer/export-templates.sh b/scripts/installer/export-templates.sh index cd44ed674e5e..c5639a865e15 100755 --- a/scripts/installer/export-templates.sh +++ b/scripts/installer/export-templates.sh @@ -29,10 +29,13 @@ if [[ "$TEMPLATE_MAJOR_VERSION" -ge "$NEW_VERSIONING_CUTOVER_MAJOR_VERSION" ]]; # New versioning (major.minor.security): the generated template files use the full version as-is. VERSION="${TEMPLATE_VERSION}" else - # Legacy versioning (major.minor.patch.security): the generated template files drop the trailing - # security component. - VERSION="${TEMPLATE_VERSION%.*}" -fi + # Legacy versioning drops the trailing security component only for four-component metadata; + # a valid three-component major.minor.patch value is already the filename version. + if [[ "$TEMPLATE_VERSION" == *.*.*.* ]]; then + VERSION="${TEMPLATE_VERSION%.*}" + else + VERSION="${TEMPLATE_VERSION}" + fi PREFIX=${4:-"systemvmtemplate-$VERSION"} CLEANUP=${2:-1} TEMP_IMAGE_PATH="/tmp/sysvm_convert/" From 569417cea45402e5fc577009b3972d9a70866a99 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 17 Sep 2026 11:07:21 -0300 Subject: [PATCH 15/15] Fix export-templates.sh syntax --- scripts/installer/export-templates.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/installer/export-templates.sh b/scripts/installer/export-templates.sh index c5639a865e15..820596837131 100755 --- a/scripts/installer/export-templates.sh +++ b/scripts/installer/export-templates.sh @@ -36,6 +36,7 @@ else else VERSION="${TEMPLATE_VERSION}" fi +fi PREFIX=${4:-"systemvmtemplate-$VERSION"} CLEANUP=${2:-1} TEMP_IMAGE_PATH="/tmp/sysvm_convert/"