Image server TLS support

This commit is contained in:
Abhisar Sinha 2026-03-31 08:51:45 +05:30 committed by Abhishek Kumar
parent e32a6ab7d9
commit 19a8509f79
6 changed files with 118 additions and 10 deletions

View File

@ -78,6 +78,12 @@ zone=default
# Generated with "uuidgen".
local.storage.uuid=
# Enable TLS for image server transfers.
# When enabled, certificate and key paths must both be configured.
# image.server.tls.enabled=false
# image.server.tls.cert.file=/etc/cloudstack/agent/cloud.crt
# image.server.tls.key.file=/etc/cloudstack/agent/cloud.key
# Location for KVM virtual router scripts.
# The path defined in this property is relative to the directory "/usr/share/cloudstack-common/".
domr.scripts.dir=scripts/network/domr/kvm

View File

@ -123,6 +123,27 @@ public class AgentProperties{
*/
public static final Property<String> LOCAL_STORAGE_PATH = new Property<>("local.storage.path", "/var/lib/libvirt/images/");
/**
* Enables TLS on the KVM image server transfer endpoint.<br>
* Data type: Boolean.<br>
* Default value: <code>false</code>
*/
public static final Property<Boolean> IMAGE_SERVER_TLS_ENABLED = new Property<>("image.server.tls.enabled", false);
/**
* PEM certificate file used by the KVM image server when TLS is enabled.<br>
* Data type: String.<br>
* Default value: <code>null</code>
*/
public static final Property<String> IMAGE_SERVER_TLS_CERT_FILE = new Property<>("image.server.tls.cert.file", null, String.class);
/**
* PEM private key file used by the KVM image server when TLS is enabled.<br>
* Data type: String.<br>
* Default value: <code>null</code>
*/
public static final Property<String> IMAGE_SERVER_TLS_KEY_FILE = new Property<>("image.server.tls.key.file", null, String.class);
/**
* Directory where Qemu sockets are placed.<br>
* These sockets are for the Qemu Guest Agent and SSVM provisioning.<br>

View File

@ -398,6 +398,9 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
private String vmActivityCheckPath;
private String nasBackupPath;
private String imageServerPath;
private boolean imageServerTlsEnabled = false;
private String imageServerTlsCertFile;
private String imageServerTlsKeyFile;
private String securityGroupPath;
private String ovsPvlanDhcpHostPath;
private String ovsPvlanVmPath;
@ -816,6 +819,18 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
return imageServerPath;
}
public boolean isImageServerTlsEnabled() {
return imageServerTlsEnabled;
}
public String getImageServerTlsCertFile() {
return imageServerTlsCertFile;
}
public String getImageServerTlsKeyFile() {
return imageServerTlsKeyFile;
}
public String getOvsPvlanDhcpHostPath() {
return ovsPvlanDhcpHostPath;
}
@ -1034,6 +1049,14 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
cachePath = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_CACHE_LOCATION);
imageServerTlsEnabled = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.IMAGE_SERVER_TLS_ENABLED);
imageServerTlsCertFile = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.IMAGE_SERVER_TLS_CERT_FILE);
imageServerTlsKeyFile = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.IMAGE_SERVER_TLS_KEY_FILE);
if (imageServerTlsEnabled && (StringUtils.isBlank(imageServerTlsCertFile) || StringUtils.isBlank(imageServerTlsKeyFile))) {
throw new ConfigurationException("image server TLS is enabled but image.server.tls.cert.file or image.server.tls.key.file is missing");
}
params.put("domr.scripts.dir", domrScriptsDir);
virtRouterResource = new VirtualRoutingResource(this);

View File

@ -40,10 +40,23 @@ import com.cloud.utils.script.Script;
public class LibvirtCreateImageTransferCommandWrapper extends CommandWrapper<CreateImageTransferCommand, Answer, LibvirtComputingResource> {
protected Logger logger = LogManager.getLogger(getClass());
private void resetService(String unitName) {
Script resetScript = new Script("/bin/bash", logger);
resetScript.add("-c");
resetScript.add(String.format("systemctl reset-failed %s || true", unitName));
resetScript.execute();
}
private static String shellQuote(String value) {
return "'" + value.replace("'", "'\\''") + "'";
}
private boolean startImageServerIfNotRunning(int imageServerPort, LibvirtComputingResource resource) {
final String imageServerPackageDir = resource.getImageServerPath();
final String imageServerParentDir = new File(imageServerPackageDir).getParent();
final String imageServerModuleName = new File(imageServerPackageDir).getName();
final String listenAddress = "0.0.0.0";
final boolean tlsEnabled = resource.isImageServerTlsEnabled();
String unitName = "cloudstack-image-server";
Script checkScript = new Script("/bin/bash", logger);
@ -54,14 +67,21 @@ public class LibvirtCreateImageTransferCommandWrapper extends CommandWrapper<Cre
return true;
}
resetService(unitName);
if (checkResult != null) {
String systemdRunCmd = String.format(
"systemd-run --unit=%s --property=Restart=no --property=WorkingDirectory=%s /usr/bin/python3 -m %s --listen 0.0.0.0 --port %d",
unitName, imageServerParentDir, imageServerModuleName, imageServerPort);
StringBuilder systemdRunCmd = new StringBuilder(String.format(
"systemd-run --unit=%s --property=Restart=no --property=WorkingDirectory=%s /usr/bin/python3 -m %s --listen %s --port %d",
unitName, shellQuote(imageServerParentDir), imageServerModuleName, shellQuote(listenAddress), imageServerPort));
if (tlsEnabled) {
systemdRunCmd.append(" --tls-enabled");
systemdRunCmd.append(" --tls-cert-file ").append(shellQuote(resource.getImageServerTlsCertFile()));
systemdRunCmd.append(" --tls-key-file ").append(shellQuote(resource.getImageServerTlsKeyFile()));
}
Script startScript = new Script("/bin/bash", logger);
startScript.add("-c");
startScript.add(systemdRunCmd);
startScript.add(systemdRunCmd.toString());
String startResult = startScript.execute();
if (startResult != null) {
@ -144,7 +164,8 @@ public class LibvirtCreateImageTransferCommandWrapper extends CommandWrapper<Cre
return new CreateImageTransferAnswer(cmd, false, "Failed to register transfer with image server.");
}
final String transferUrl = String.format("http://%s:%d/images/%s", resource.getPrivateIp(), imageServerPort, transferId);
final String transferScheme = resource.isImageServerTlsEnabled() ? "https" : "http";
final String transferUrl = String.format("%s://%s:%d/images/%s", transferScheme, resource.getPrivateIp(), imageServerPort, transferId);
return new CreateImageTransferAnswer(cmd, true, "Image transfer prepared on KVM host.", transferId, transferUrl);
}
}

View File

@ -39,9 +39,8 @@ public class LibvirtFinalizeImageTransferCommandWrapper extends CommandWrapper<F
resetScript.execute();
}
private boolean stopImageServer() {
private boolean stopImageServer(int imageServerPort) {
String unitName = "cloudstack-image-server";
final int imageServerPort = LibvirtComputingResource.IMAGE_SERVER_DEFAULT_PORT;
Script checkScript = new Script("/bin/bash", logger);
checkScript.add("-c");
@ -81,6 +80,7 @@ public class LibvirtFinalizeImageTransferCommandWrapper extends CommandWrapper<F
public Answer execute(FinalizeImageTransferCommand cmd, LibvirtComputingResource resource) {
final String transferId = cmd.getTransferId();
final int imageServerPort = LibvirtComputingResource.IMAGE_SERVER_DEFAULT_PORT;
if (StringUtils.isBlank(transferId)) {
return new Answer(cmd, false, "transferId is empty.");
}
@ -88,12 +88,12 @@ public class LibvirtFinalizeImageTransferCommandWrapper extends CommandWrapper<F
int activeTransfers = ImageServerControlSocket.unregisterTransfer(transferId);
if (activeTransfers < 0) {
logger.warn("Could not reach image server to unregister transfer {}; assuming server is down", transferId);
stopImageServer();
stopImageServer(imageServerPort);
return new Answer(cmd, true, "Image transfer finalized (server unreachable, forced stop).");
}
if (activeTransfers == 0) {
stopImageServer();
stopImageServer(imageServerPort);
}
return new Answer(cmd, true, "Image transfer finalized.");

View File

@ -20,6 +20,7 @@ import json
import logging
import os
import socket
import ssl
import threading
from http.server import HTTPServer
from socketserver import ThreadingMixIn
@ -184,8 +185,26 @@ def main() -> None:
default=CONTROL_SOCKET,
help="Path to the Unix domain control socket",
)
parser.add_argument(
"--tls-enabled",
action="store_true",
help="Enable TLS for the HTTP transfer endpoint",
)
parser.add_argument(
"--tls-cert-file",
default=None,
help="Path to PEM certificate file used when TLS is enabled",
)
parser.add_argument(
"--tls-key-file",
default=None,
help="Path to PEM private key file used when TLS is enabled",
)
args = parser.parse_args()
if args.tls_enabled and (not args.tls_cert_file or not args.tls_key_file):
parser.error("--tls-enabled requires --tls-cert-file and --tls-key-file")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
@ -204,5 +223,23 @@ def main() -> None:
addr = (args.listen, args.port)
httpd = ThreadingHTTPServer(addr, handler_cls)
logging.info("listening on http://%s:%d", args.listen, args.port)
scheme = "http"
if args.tls_enabled:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
if hasattr(ssl, "TLSVersion") and hasattr(context, "minimum_version"):
context.minimum_version = ssl.TLSVersion.TLSv1_2
else:
if hasattr(ssl, "OP_NO_TLSv1"):
context.options |= ssl.OP_NO_TLSv1
if hasattr(ssl, "OP_NO_TLSv1_1"):
context.options |= ssl.OP_NO_TLSv1_1
context.load_cert_chain(certfile=args.tls_cert_file, keyfile=args.tls_key_file)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
scheme = "https"
logging.info("listening on %s://%s:%d", scheme, args.listen, args.port)
httpd.serve_forever()