This is an automated email from the ASF dual-hosted git repository.

nvazquez pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 602d9ec3e03 engine-schema: upgrade path for 24.0.0 (#14033)
602d9ec3e03 is described below

commit 602d9ec3e03e6350b55dd1349118ea2ccb766cca
Author: Abhishek Kumar <[email protected]>
AuthorDate: Fri Sep 18 15:48:51 2026 +0530

    engine-schema: upgrade path for 24.0.0 (#14033)
    
    * engine-schema: upgrade path for 24.0.0
    
    Adds a 4.23.0 to 24.0.0 upgrade path.
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    
    * fix CS version
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    
    * fix upgrade unit tests for cutover
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    
    * fix imports
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    
    * fix template version
    
    * continue using 3 part version for sys template
    
    * fix
    
    * fix more
    
    * use security version after cutover
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    
    * improvements and add tests
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    
    * fix more scripts
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    
    * address remaining comments
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    
    * more changes
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    
    * Update versioning logic in export-templates.sh
    
    Refactor versioning logic to handle four-component metadata separately.
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * Fix export-templates.sh syntax
    
    ---------
    
    Signed-off-by: Abhishek Kumar <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    Co-authored-by: nvazquez <[email protected]>
---
 engine/schema/pom.xml                              |  14 ++
 .../com/cloud/upgrade/DatabaseUpgradeChecker.java  |  21 ++-
 .../com/cloud/upgrade/dao/Upgrade42300to2400.java  |  30 +++++
 .../META-INF/db/schema-42300to2400-cleanup.sql     |  20 +++
 .../resources/META-INF/db/schema-42300to2400.sql   |  20 +++
 .../cloud/upgrade/DatabaseUpgradeCheckerTest.java  | 112 +++++++++++-----
 .../upgrade/SystemVmTemplateRegistrationTest.java  |  37 ++++++
 engine/schema/templateConfig.sh                    |  75 +++++++----
 engine/schema/test_templateConfig.sh               | 112 ++++++++++++++++
 .../apache/cloudstack/veeam/api/dto/Version.java   |   6 +-
 .../cloudstack/veeam/api/dto/VersionTest.java      |  34 +++++
 scripts/installer/export-templates.sh              |  18 ++-
 .../nicira/nvp/plugin/NiciraNvpApiVersion.java     |   2 +-
 .../apache/cloudstack/utils/CloudStackVersion.java | 148 ++++++++++++++++++---
 .../cloudstack/utils/CloudStackVersionTest.java    | 116 ++++++++++++++--
 15 files changed, 680 insertions(+), 85 deletions(-)

diff --git a/engine/schema/pom.xml b/engine/schema/pom.xml
index a7d53d01b5f..6e078d7ec9f 100644
--- a/engine/schema/pom.xml
+++ b/engine/schema/pom.xml
@@ -153,6 +153,20 @@
                 <artifactId>exec-maven-plugin</artifactId>
                 <version>${cs.exec-maven-plugin.version}</version>
                 <executions>
+                    <execution>
+                        <id>test-templateConfig</id>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>exec</goal>
+                        </goals>
+                        <configuration>
+                            <workingDirectory>${basedir}/</workingDirectory>
+                            <executable>bash</executable>
+                            <arguments>
+                                <argument>test_templateConfig.sh</argument>
+                            </arguments>
+                        </configuration>
+                    </execution>
                     <execution>
                         <id>systemvm-template-metadata</id>
                         <phase>package</phase>
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 c3a982aa70e..57ae2c3d649 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.Upgrade42000to42010;
 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 class DatabaseUpgradeChecker implements 
SystemIntegrityChecker {
                 .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();
     }
 
@@ -513,8 +515,7 @@ public class DatabaseUpgradeChecker implements 
SystemIntegrityChecker {
             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());
+            updateSystemVmTemplateVersion(sysVmVersion);
 
             LOGGER.info("DB version = {} Code Version = {}", dbVersion, 
currentVersion);
 
@@ -540,6 +541,18 @@ public class DatabaseUpgradeChecker implements 
SystemIntegrityChecker {
         }
     }
 
+    /**
+     * 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 &lt; 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.getTinyRelease());
+    }
+
     /**
      * 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/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 00000000000..ce217cef9e7
--- /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 00000000000..861a038fe7a
--- /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 00000000000..7c11013a17d
--- /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
+--;
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 884398cf410..aa39567af84 100644
--- 
a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java
+++ 
b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java
@@ -16,20 +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.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;
@@ -47,6 +52,7 @@ import com.cloud.upgrade.dao.Upgrade41610to41700;
 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;
@@ -55,15 +61,8 @@ import com.cloud.upgrade.dao.Upgrade470to471;
 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 {
@@ -81,6 +80,8 @@ public class DatabaseUpgradeCheckerTest {
     ResultSet resultSet;
 
     private DataSource backupDataSource;
+    private String previousCsMajorVersion;
+    private String previousCsTinyVersion;
 
     @Before
     public void setup() throws Exception {
@@ -92,6 +93,9 @@ public class DatabaseUpgradeCheckerTest {
         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
@@ -99,6 +103,9 @@ public class DatabaseUpgradeCheckerTest {
         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
@@ -214,10 +221,10 @@ public class DatabaseUpgradeCheckerTest {
     @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 +241,7 @@ public class DatabaseUpgradeCheckerTest {
         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 +275,7 @@ public class DatabaseUpgradeCheckerTest {
         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 +297,7 @@ public class DatabaseUpgradeCheckerTest {
         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 +307,26 @@ public class DatabaseUpgradeCheckerTest {
         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 +358,7 @@ public class DatabaseUpgradeCheckerTest {
         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);
@@ -422,4 +430,48 @@ public class DatabaseUpgradeCheckerTest {
         assertTrue(upgrades[2] instanceof Upgrade42040to42100);
         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"));
+
+        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 51db952eb61..dfc67b1dece 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 class SystemVmTemplateRegistrationTest {
         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 21a4ee79574..7a8283e77c1 100755
--- a/engine/schema/templateConfig.sh
+++ b/engine/schema/templateConfig.sh
@@ -17,16 +17,40 @@
 # 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() {
   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")"
+  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. 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, 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/";
 }
 
@@ -64,7 +88,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 +105,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 00000000000..2acb4281edb
--- /dev/null
+++ b/engine/schema/test_templateConfig.sh
@@ -0,0 +1,112 @@
+#!/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"
+
+# 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"
+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"
+
+# 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() {
+  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/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 7b7d80a0f16..5e1aa52533c 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,10 @@ public final class Version {
         }
         version.setMajor(String.valueOf(csVersion.getMajorRelease()));
         version.setMinor(String.valueOf(csVersion.getMinorRelease()));
-        version.setBuild(String.valueOf(csVersion.getPatchRelease()));
-        version.setRevision(String.valueOf(csVersion.getSecurityRelease()));
+        version.setBuild(String.valueOf(csVersion.getTinyRelease()));
+        if (!csVersion.usesNewVersioning()) {
+            
version.setRevision(String.valueOf(csVersion.getSecurityRelease()));
+        }
         return version;
     }
 }
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 a91f193ba42..72f1d92755e 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 class VersionTest {
         }
     }
 
+    @Test
+    public void 
fromPackageAndCSVersion_NewVersioningZeroSecurity_MapsSecurityToBuildAndOmitsRevision()
 {
+        CloudStackVersion csVersion = CloudStackVersion.parse("24.0.0");
+        try (MockedStatic<VeeamControlService> 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<VeeamControlService> 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<VeeamControlService> mocked = 
Mockito.mockStatic(VeeamControlService.class)) {
diff --git a/scripts/installer/export-templates.sh 
b/scripts/installer/export-templates.sh
index dbd560e44c7..82059683713 100755
--- a/scripts/installer/export-templates.sh
+++ b/scripts/installer/export-templates.sh
@@ -16,11 +16,27 @@
 # 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"
 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 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
+fi
 PREFIX=${4:-"systemvmtemplate-$VERSION"}
 CLEANUP=${2:-1}
 TEMP_IMAGE_PATH="/tmp/sysvm_convert/"
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 7e4dd5b19b4..5c79d78a52a 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 class NiciraNvpApiVersion {
         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 e29bd9c4e17..fd15df7d5ea 100644
--- a/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java
+++ b/utils/src/main/java/org/apache/cloudstack/utils/CloudStackVersion.java
@@ -39,28 +39,40 @@ import static 
com.google.common.base.Preconditions.checkState;
  */
 public final class CloudStackVersion implements Comparable<CloudStackVersion> {
 
-    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)?");
+    // 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 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 int 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 >= 0, CloudStackVersion.class.getName() + 
"(int, int, int, Integer) requires a patchRelease greater than 0.");
-        checkArgument((securityRelease != null && securityRelease >= 0) || 
(securityRelease == null),
+        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;
         this.minorRelease = minorRelease;
         this.patchRelease = patchRelease;
         this.securityRelease = securityRelease;
+        this.external = external;
 
     }
 
@@ -69,11 +81,15 @@ public final class CloudStackVersion implements 
Comparable<CloudStackVersion> {
      * Parses a <code>String</code> representation of a version that conforms 
one of the following
      * formats into a <code>CloudStackVersion</code> instance:
      * <ul>
-     *     
<li><code>&lt;major&gt;.&lt;minor&gt;.&lt;patch&gt;.&lt;security&gt;</code></li>
-     *     
<li><code>&lt;major&gt;.&lt;minor&gt;.&lt;patch&gt;.&lt;security&gt;.&lt;security&gt;</code></li>
-     *     
<li><code>&lt;major&gt;.&lt;minor&gt;.&lt;patch&gt;.&lt;security&gt;.&lt;security&gt;-&lt;any
 string&gt;</code></li>
+     *     <li><code>&lt;major&gt;.&lt;minor&gt;.&lt;patch&gt;</code> (legacy, 
deprecated since 24.0.0, allowed only below major version 24)</li>
+     *     
<li><code>&lt;major&gt;.&lt;minor&gt;.&lt;patch&gt;.&lt;security&gt;</code> 
(legacy, deprecated since 24.0.0, allowed only below major version 24)</li>
+     *     <li><code>&lt;major&gt;.&lt;minor&gt;.&lt;security 
release&gt;</code> (for versions &gt;= 24.0.0)</li>
      * </ul>
      *
+     * Legacy patch-based formats remain supported for backward compatibility, 
e.g. <code>4.23.0.1-SNAPSHOT</code>,
+     * but only below major version 24: a 4-position value whose major release 
is at or above 24, e.g.
+     * <code>24.0.0.1</code>, is rejected.
+     *
      * If the string contains a suffix that begins with a "-" character, then 
the "-" and all characters following it
      * will be dropped.
      *
@@ -85,13 +101,33 @@ public final class CloudStackVersion implements 
Comparable<CloudStackVersion> {
      *
      */
     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
+     * <code>true</code>, always applies the legacy 
major.minor.patch(.security) component mapping and
+     * never the major-24-and-above new-versioning cutover rule.
+     * <p>
+     * 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 <code>external = 
true</code> so a value like
+     * <code>24.0.1</code> 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 <code>value</code> parsed into a <code>CloudStackVersion</code> 
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, "-");
 
         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)(-<legacy patch>)");
+                value + ", but requires a value in the format of 
int.int.int(.int)(-<suffix>)");
 
         final String[] components = trimmedValue.split("\\.");
 
@@ -100,10 +136,28 @@ public final class CloudStackVersion implements 
Comparable<CloudStackVersion> {
 
         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(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 (!external && 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);
+        return new CloudStackVersion(majorRelease, minorRelease, patchRelease, 
securityRelease, external);
 
     }
 
@@ -120,7 +174,26 @@ public final class CloudStackVersion implements 
Comparable<CloudStackVersion> {
      * @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 <code>external = true</code> 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 <code>version1</code>/<code>version2</code> 
come from a version scheme other
+     *                 than CloudStack's own
+     *
+     * @return A value less than zero (0) indicates <code>version1</code> is 
less than <code>version2</code>.  A value
+     *         equal to zero (0) indicates <code>version1</code> equals 
<code>version2</code>.  A value greater than zero (0)
+     *         indicates <code>version1</code> is greater than 
<code>version2</code>.
+     */
+    public static int compare(String version1, String version2, boolean 
external) {
+        return parse(version1, external).compareTo(parse(version2, external));
     }
 
     /**
@@ -187,11 +260,28 @@ public final class CloudStackVersion implements 
Comparable<CloudStackVersion> {
     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";
+        // 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";
+    }
+
+    /**
+     * Whether {@link #parse(String)} would accept the given value, following 
the same cutover rule that
+     * rejects a 4-position <code>major.minor.patch.security</code> 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<Integer> normalizeVersionValues(final 
ImmutableList<Integer> values) {
@@ -207,6 +297,14 @@ public final class CloudStackVersion implements 
Comparable<CloudStackVersion> {
 
     }
 
+    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 +342,21 @@ public final class CloudStackVersion implements 
Comparable<CloudStackVersion> {
         return securityRelease;
     }
 
+    public boolean usesNewVersioning() {
+        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
     public boolean equals(final Object thatObject) {
 
@@ -270,6 +383,11 @@ public final class CloudStackVersion implements 
Comparable<CloudStackVersion> {
 
     @Override
     public String toString() {
+        // Canonicalize cutover-and-later versions to 
major.minor.securityRelease.
+        if (securityRelease != null && patchRelease == 0 && 
usesNewVersioning()) {
+            return Joiner.on(".").join(ImmutableList.of(majorRelease, 
minorRelease, securityRelease));
+        }
+
         return Joiner.on(".").join(asList());
     }
 
@@ -279,7 +397,7 @@ public final class CloudStackVersion implements 
Comparable<CloudStackVersion> {
      */
     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 dabaf9bc97d..e9bbb35af3d 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 final class CloudStackVersionTest {
         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,18 +78,67 @@ public final class CloudStackVersionTest {
         "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);
     }
 
+    @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) {
 
@@ -147,7 +222,9 @@ public final class CloudStackVersionTest {
         "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 +255,9 @@ public final class CloudStackVersionTest {
         "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) {
 
@@ -196,9 +275,11 @@ public final class CloudStackVersionTest {
         "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.0.0"
     })
     public void testTrimRouterVersion(final String value, final String 
expected) {
 
@@ -206,6 +287,18 @@ public final class CloudStackVersionTest {
 
     }
 
+    @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));
@@ -213,6 +306,7 @@ public final class CloudStackVersionTest {
             
Assert.assertEquals(CloudStackVersion.getVMwareParentVersion(hypervisorVersion),
 expectedParentVersion);
         }
     }
+
     @Test
     public void testGetParentVersion() {
         verifyGetVMwareParentVersion(null, null);
@@ -223,5 +317,11 @@ public final class CloudStackVersionTest {
         verifyGetVMwareParentVersion("8.0.0", "8.0");
         verifyGetVMwareParentVersion("8.0.0.2", "8.0");
         verifyGetVMwareParentVersion("8.0.1.0", "8.0.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");
     }
 }

Reply via email to