Skip to content

Commit df47ea1

Browse files
committed
systemvm-template: support on-demand download during setup and registration
Bundling all hypervisor SystemVM templates in release packages simplifies installs but inflates build time and artifact size. This change enables downloading templates on demand when they’re not found after package installation. The download path is wired into both cloud-setup-management and the existing SystemVM template registration flow. For connected or mirrored environments, a repository URL prefix can be provided to support air-gapped setups: pass --systemvm-templates-repository <URL-prefix> to cloud-setup-management, or set system.vm.templates.download.repository=<URL-prefix> in server.properties for post-setup registration. If templates are already present (bundled or preseeded), behavior is unchanged and no download is attempted. Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent 8089d32 commit df47ea1

File tree

6 files changed

+224
-36
lines changed

6 files changed

+224
-36
lines changed

client/bindir/cloud-setup-management.in

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,100 @@ from cloudutils.cloudException import CloudRuntimeException, CloudInternalExcept
3636
from cloudutils.globalEnv import globalEnv
3737
from cloudutils.serviceConfigServer import cloudManagementConfig
3838
from optparse import OptionParser
39+
import urllib.request
40+
import configparser
41+
import hashlib
42+
43+
SYSTEMVM_TEMPLATES_PATH = "/usr/share/cloudstack-management/templates/systemvm"
44+
SYSTEMVM_TEMPLATES_METADATA_FILE = SYSTEMVM_TEMPLATES_PATH + "/metadata.ini"
45+
46+
def verify_sha512_checksum(file_path, expected_checksum):
47+
sha512 = hashlib.sha512()
48+
try:
49+
with open(file_path, "rb") as f:
50+
for chunk in iter(lambda: f.read(8192), b""):
51+
sha512.update(chunk)
52+
return sha512.hexdigest().lower() == expected_checksum.lower()
53+
except Exception as e:
54+
print(f"Failed to verify checksum for {file_path}: {e}")
55+
return False
56+
57+
def download_file(url, dest_path, chunk_size=8 * 1024 * 1024):
58+
"""
59+
Downloads a file from the given URL to the specified destination path in chunks.
60+
"""
61+
try:
62+
with urllib.request.urlopen(url) as response:
63+
total_size = response.length if response.length else None
64+
downloaded = 0
65+
try:
66+
with open(dest_path, 'wb') as out_file:
67+
while True:
68+
chunk = response.read(chunk_size)
69+
if not chunk:
70+
break
71+
out_file.write(chunk)
72+
downloaded += len(chunk)
73+
if total_size:
74+
print(f"Downloaded {downloaded / (1024 * 1024):.2f}MB of {total_size / (1024 * 1024):.2f}MB", end='\r')
75+
except PermissionError as pe:
76+
print(f"Permission denied: {dest_path}")
77+
raise
78+
print(f"\nDownloaded file from {url} to {dest_path}")
79+
except Exception as e:
80+
print(f"Failed to download file: {e}")
81+
raise
82+
83+
def download_template_if_needed(template, url, filename, checksum):
84+
dest_path = os.path.join(SYSTEMVM_TEMPLATES_PATH, filename)
85+
if os.path.exists(dest_path):
86+
if checksum and verify_sha512_checksum(dest_path, checksum):
87+
print(f"{template} System VM template already exists at {dest_path} with valid checksum, skipping download.")
88+
return
89+
else:
90+
print(f"{template} System VM template at {dest_path} has invalid or missing checksum, re-downloading...")
91+
else:
92+
print(f"Downloading {template} System VM template from {url} to {dest_path}...")
93+
try:
94+
download_file(url, dest_path)
95+
except Exception as e:
96+
print(f"ERROR: Failed to download {template} System VM template: {e}")
97+
98+
def collect_template_metadata(selected_templates, options):
99+
template_metadata_list = []
100+
if not os.path.exists(SYSTEMVM_TEMPLATES_METADATA_FILE):
101+
print(f"ERROR: System VM templates metadata file not found at {SYSTEMVM_TEMPLATES_METADATA_FILE}, cannot download templates.")
102+
sys.exit(1)
103+
config = configparser.ConfigParser()
104+
config.read(SYSTEMVM_TEMPLATES_METADATA_FILE)
105+
template_repo_url = None
106+
if options.systemvm_templates_repository:
107+
if "default" in config and "downloadrepository" in config["default"]:
108+
template_repo_url = config["default"]["downloadrepository"].strip()
109+
if not template_repo_url:
110+
print("ERROR: downloadrepository value is empty in metadata file, cannot use --systemvm-template-repository option.")
111+
sys.exit(1)
112+
for template in selected_templates:
113+
if template in config:
114+
url = config[template].get("downloadurl")
115+
filename = config[template].get("filename")
116+
checksum = config[template].get("checksum")
117+
if url and filename:
118+
if template_repo_url:
119+
url = url.replace(template_repo_url, options.systemvm_templates_repository)
120+
template_metadata_list.append({
121+
"template": template,
122+
"url": url,
123+
"filename": filename,
124+
"checksum": checksum
125+
})
126+
else:
127+
print(f"ERROR: URL or filename not found for {template} System VM template in metadata.")
128+
sys.exit(1)
129+
else:
130+
print(f"ERROR: No metadata found for {template} System VM template.")
131+
sys.exit(1)
132+
return template_metadata_list
39133

40134
if __name__ == '__main__':
41135
initLoging("@MSLOGDIR@/setupManagement.log")
@@ -45,6 +139,16 @@ if __name__ == '__main__':
45139
parser.add_option("--https", action="store_true", dest="https", help="Enable HTTPs connection of management server")
46140
parser.add_option("--tomcat7", action="store_true", dest="tomcat7", help="Depreciated option, don't use it")
47141
parser.add_option("--no-start", action="store_true", dest="nostart", help="Do not start management server after successful configuration")
142+
parser.add_option(
143+
"--systemvm-templates",
144+
dest="systemvm_templates",
145+
help="Specify System VM templates to download: all, kvm-aarch64, kvm-x86_64, xenserver, vmware or comma-separated list of hypervisor combinations (e.g., kvm-x86_64,xenserver). Default is kvm-x86_64.",
146+
)
147+
parser.add_option(
148+
"--systemvm-templates-repository",
149+
dest="systemvm_templates_repository",
150+
help="Specify the URL to download System VM templates from."
151+
)
48152
(options, args) = parser.parse_args()
49153
if options.https:
50154
glbEnv.svrMode = "HttpsServer"
@@ -53,6 +157,22 @@ if __name__ == '__main__':
53157
if options.nostart:
54158
glbEnv.noStart = True
55159

160+
available_templates = ["kvm-aarch64", "kvm-x86_64", "xenserver", "vmware"]
161+
templates_arg = options.systemvm_templates
162+
163+
selected_templates = ["kvm-x86_64"]
164+
if templates_arg:
165+
templates_list = [t.strip().lower() for t in templates_arg.split(",")]
166+
if "all" in templates_list:
167+
selected_templates = available_templates
168+
else:
169+
selected_templates = [t for t in templates_list if t in available_templates]
170+
print(f"Selected systemvm templates to download: {', '.join(selected_templates) if selected_templates else 'None'}")
171+
172+
template_metadata_list = []
173+
if selected_templates:
174+
template_metadata_list = collect_template_metadata(selected_templates, options)
175+
56176
glbEnv.mode = "Server"
57177

58178
print("Starting to configure CloudStack Management Server:")
@@ -68,9 +188,13 @@ if __name__ == '__main__':
68188
print("CloudStack Management Server setup is Done!")
69189
print("Please ensure ports 8080, 8250, 8443, and 9090 are opened and not firewalled for the management server and not in use by other processes on this host.")
70190
except (CloudRuntimeException, CloudInternalException) as e:
191+
71192
print(e)
72193
print("Try to restore your system:")
73194
try:
74195
syscfg.restore()
75196
except:
76197
pass
198+
199+
for meta in template_metadata_list:
200+
download_template_if_needed(meta["template"], meta["url"], meta["filename"], meta["checksum"])

client/conf/server.properties.in

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,8 @@ extensions.deployment.mode=@EXTENSIONSDEPLOYMENTMODE@
6262
# Thread pool configuration
6363
#threads.min=10
6464
#threads.max=500
65+
66+
# The URL prefix for the system VM templates repository. When downloading system VM templates, the server replaces the
67+
# `downloadrepository` key value from the metadata file in template URLs. If not specified, the original template URL
68+
# will be for download.
69+
# system.vm.templates.download.repository=http://download.cloudstack.org/systemvm/4.20/

engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ public class SystemVmTemplateRegistration {
108108
private static Integer LINUX_12_ID = 363;
109109
private static final Integer SCRIPT_TIMEOUT = 1800000;
110110
private static final Integer LOCK_WAIT_TIMEOUT = 1200;
111+
protected static final String TEMPLATES_DOWNLOAD_REPOSITORY_KEY = "downloadurl";
112+
protected static final String TEMPLATES_CUSTOM_DOWNLOAD_REPOSITORY_KEY = "system.vm.templates.download.repository";
111113
protected static final List<CPU.CPUArch> DOWNLOADABLE_TEMPLATE_ARCH_TYPES = Arrays.asList(
114+
CPU.CPUArch.amd64,
112115
CPU.CPUArch.arm64
113116
);
114117

@@ -820,6 +823,14 @@ public static String parseMetadataFile() {
820823
LOGGER.error(errMsg);
821824
throw new CloudRuntimeException(errMsg);
822825
}
826+
Ini.Section defaultSection = ini.get("default");
827+
boolean updateCustomDownloadRepository = false;
828+
String defaultDownloadRepository = defaultSection.get(TEMPLATES_DOWNLOAD_REPOSITORY_KEY);
829+
String customDownloadRepository = System.getProperty(TEMPLATES_CUSTOM_DOWNLOAD_REPOSITORY_KEY);
830+
if (StringUtils.isNotBlank(customDownloadRepository) && StringUtils.isNotBlank(defaultDownloadRepository)) {
831+
LOGGER.debug("Updating custom download repository: {}", customDownloadRepository);
832+
updateCustomDownloadRepository = true;
833+
}
823834
for (Pair<Hypervisor.HypervisorType, CPU.CPUArch> hypervisorType : hypervisorList) {
824835
String key = getHypervisorArchKey(hypervisorType.first(), hypervisorType.second());
825836
Ini.Section section = ini.get(key);
@@ -828,16 +839,21 @@ public static String parseMetadataFile() {
828839
key, metadataFilePath);
829840
continue;
830841
}
842+
String url = section.get("downloadurl");
843+
if (StringUtils.isNotBlank(url) && updateCustomDownloadRepository) {
844+
url = url.replaceFirst(defaultDownloadRepository.trim(),
845+
customDownloadRepository.trim());
846+
LOGGER.info("Updated download URL for {} to {}", key, url);
847+
}
831848
NewTemplateMap.put(key, new MetadataTemplateDetails(
832849
hypervisorType.first(),
833850
section.get("templatename"),
834851
section.get("filename"),
835-
section.get("downloadurl"),
852+
url,
836853
section.get("checksum"),
837854
hypervisorType.second(),
838855
section.get("guestos")));
839856
}
840-
Ini.Section defaultSection = ini.get("default");
841857
return defaultSection.get("version").trim();
842858
}
843859

engine/schema/templateConfig.sh

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ function getTemplateVersion() {
2727
export CS_VERSION="${subversion1}"."${subversion2}"
2828
export CS_MINOR_VERSION="${minorversion}"
2929
export VERSION="${CS_VERSION}.${CS_MINOR_VERSION}"
30+
export CS_SYSTEMTEMPLATE_REPO="https://download.cloudstack.org/systemvm/${CS_VERSION}/"
3031
}
3132

3233
function getGenericName() {
@@ -63,7 +64,7 @@ function getChecksum() {
6364

6465
function createMetadataFile() {
6566
local fileData=$(cat $SOURCEFILE)
66-
echo -e "["default"]\nversion = $VERSION.${securityversion}\n" >> $METADATAFILE
67+
echo -e "["default"]\nversion = $VERSION.${securityversion}\ndownloadrepository = $CS_SYSTEMTEMPLATE_REPO\n" >> $METADATAFILE
6768
for template in "${templates[@]}"
6869
do
6970
section="${template%%:*}"
@@ -82,13 +83,21 @@ function createMetadataFile() {
8283

8384
declare -a templates
8485
getTemplateVersion $1
85-
templates=( "kvm-x86_64:https://download.cloudstack.org/systemvm/${CS_VERSION}/systemvmtemplate-$VERSION-x86_64-kvm.qcow2.bz2"
86-
"kvm-aarch64:https://download.cloudstack.org/systemvm/${CS_VERSION}/systemvmtemplate-$VERSION-aarch64-kvm.qcow2.bz2"
87-
"vmware:https://download.cloudstack.org/systemvm/${CS_VERSION}/systemvmtemplate-$VERSION-x86_64-vmware.ova"
88-
"xenserver:https://download.cloudstack.org/systemvm/$CS_VERSION/systemvmtemplate-$VERSION-x86_64-xen.vhd.bz2"
89-
"hyperv:https://download.cloudstack.org/systemvm/$CS_VERSION/systemvmtemplate-$VERSION-x86_64-hyperv.vhd.zip"
90-
"lxc:https://download.cloudstack.org/systemvm/$CS_VERSION/systemvmtemplate-$VERSION-x86_64-kvm.qcow2.bz2"
91-
"ovm3:https://download.cloudstack.org/systemvm/$CS_VERSION/systemvmtemplate-$VERSION-x86_64-ovm.raw.bz2" )
86+
declare -A template_specs=(
87+
[kvm-x86_64]="x86_64-kvm.qcow2.bz2"
88+
[kvm-aarch64]="aarch64-kvm.qcow2.bz2"
89+
[vmware]="x86_64-vmware.ova"
90+
[xenserver]="x86_64-xen.vhd.bz2"
91+
[hyperv4]="x86_64-hyperv.vhd.zip"
92+
[lxc]="x86_64-kvm.qcow2.bz2"
93+
[ovm3]="x86_64-ovm.raw.bz2"
94+
)
95+
96+
templates=()
97+
for key in "${!template_specs[@]}"; do
98+
url="${CS_SYSTEMTEMPLATE_REPO}/systemvmtemplate-$VERSION-${template_specs[$key]}"
99+
templates+=("$key:$url")
100+
done
92101

93102
PARENTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/dist/systemvm-templates/"
94103
mkdir -p $PARENTPATH

plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
import java.io.BufferedReader;
2222
import java.io.File;
23-
import java.io.FileInputStream;
2423
import java.io.IOException;
2524
import java.io.InputStreamReader;
2625
import java.nio.file.Files;
@@ -51,6 +50,7 @@
5150
import org.apache.cloudstack.api.ApiConstants;
5251
import org.apache.cloudstack.extension.Extension;
5352
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
53+
import org.apache.cloudstack.utils.ServerPropertiesUtil;
5454
import org.apache.cloudstack.utils.security.DigestHelper;
5555
import org.apache.commons.collections.CollectionUtils;
5656
import org.apache.commons.collections.MapUtils;
@@ -77,7 +77,6 @@
7777
import com.cloud.serializer.GsonHelper;
7878
import com.cloud.utils.FileUtil;
7979
import com.cloud.utils.Pair;
80-
import com.cloud.utils.PropertiesUtil;
8180
import com.cloud.utils.StringUtils;
8281
import com.cloud.utils.component.ManagerBase;
8382
import com.cloud.utils.component.PluggableService;
@@ -208,29 +207,6 @@ protected void createOrCheckExtensionsDataDirectory() throws ConfigurationExcept
208207
logger.info("Extensions data directory path: {}", extensionsDataDirectory);
209208
}
210209

211-
private String getServerProperty(String name) {
212-
Properties props = propertiesRef.get();
213-
if (props == null) {
214-
File propsFile = PropertiesUtil.findConfigFile(PROPERTIES_FILE);
215-
if (propsFile == null) {
216-
logger.error("{} file not found", PROPERTIES_FILE);
217-
return null;
218-
}
219-
Properties tempProps = new Properties();
220-
try (FileInputStream is = new FileInputStream(propsFile)) {
221-
tempProps.load(is);
222-
} catch (IOException e) {
223-
logger.error("Error loading {}: {}", PROPERTIES_FILE, e.getMessage(), e);
224-
return null;
225-
}
226-
if (!propertiesRef.compareAndSet(null, tempProps)) {
227-
tempProps = propertiesRef.get();
228-
}
229-
props = tempProps;
230-
}
231-
return props.getProperty(name);
232-
}
233-
234210
@Override
235211
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
236212
super.configure(name, params);
@@ -242,7 +218,7 @@ public boolean configure(String name, Map<String, Object> params) throws Configu
242218
}
243219

244220
private void initializeExtensionDirectories() {
245-
String deploymentMode = getServerProperty(EXTENSIONS_DEPLOYMENT_MODE_NAME);
221+
String deploymentMode = ServerPropertiesUtil.getProperty(EXTENSIONS_DEPLOYMENT_MODE_NAME);
246222
if ("developer".equals(deploymentMode)) {
247223
extensionsDirectory = EXTENSIONS_DIRECTORY_DEV;
248224
extensionsDataDirectory = EXTENSIONS_DATA_DIRECTORY_DEV;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.utils;
19+
20+
import java.io.File;
21+
import java.io.FileInputStream;
22+
import java.io.IOException;
23+
import java.util.Properties;
24+
import java.util.concurrent.atomic.AtomicReference;
25+
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
29+
import com.cloud.utils.PropertiesUtil;
30+
31+
public class ServerPropertiesUtil {
32+
private static final Logger logger = LoggerFactory.getLogger(ServerPropertiesUtil.class);
33+
private static final String PROPERTIES_FILE = "server.properties";
34+
private static final AtomicReference<Properties> propertiesRef = new AtomicReference<>();
35+
36+
public static String getProperty(String name) {
37+
Properties props = propertiesRef.get();
38+
if (props != null) {
39+
return props.getProperty(name);
40+
}
41+
File propsFile = PropertiesUtil.findConfigFile(PROPERTIES_FILE);
42+
if (propsFile == null) {
43+
logger.error("{} file not found", PROPERTIES_FILE);
44+
return null;
45+
}
46+
Properties tempProps = new Properties();
47+
try (FileInputStream is = new FileInputStream(propsFile)) {
48+
tempProps.load(is);
49+
} catch (IOException e) {
50+
logger.error("Error loading {}: {}", PROPERTIES_FILE, e.getMessage(), e);
51+
return null;
52+
}
53+
if (!propertiesRef.compareAndSet(null, tempProps)) {
54+
tempProps = propertiesRef.get();
55+
}
56+
return tempProps.getProperty(name);
57+
}
58+
}

0 commit comments

Comments
 (0)