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 < 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><major>.<minor>.<patch>.<security></code></li>
- *
<li><code><major>.<minor>.<patch>.<security>.<security></code></li>
- *
<li><code><major>.<minor>.<patch>.<security>.<security>-<any
string></code></li>
+ * <li><code><major>.<minor>.<patch></code> (legacy,
deprecated since 24.0.0, allowed only below major version 24)</li>
+ *
<li><code><major>.<minor>.<patch>.<security></code>
(legacy, deprecated since 24.0.0, allowed only below major version 24)</li>
+ * <li><code><major>.<minor>.<security
release></code> (for versions >= 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");
}
}