Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
43a36f5
storage: enable RBD/Ceph volume encryption support (shared base)
Jun 25, 2026
e599375
kvm: Ceph-native LUKS2 encryption for RBD volumes (engine='librbd')
Jun 25, 2026
d16730c
kvm: gate host encryption probe on librbd support for RBD
Jun 25, 2026
af6b8d7
kvm: resize support for librbd-encrypted RBD volumes (#5)
Jun 25, 2026
a729f6f
kvm: route online resize of encrypted RBD through virsh blockresize
Jun 25, 2026
559af2a
kvm: encrypted RBD root disks (thin CoW clone + full-copy fallback)
Jul 4, 2026
b27d33b
kvm: harden and align librbd-encrypted RBD volume code
Jul 7, 2026
dca2494
kvm: add RbdEncryption unit tests
Jul 7, 2026
bf87c88
kvm: refuse encrypted RBD hot-plug on libvirt < 10.1.0
Jul 7, 2026
bc31693
docs: add PendingReleaseNotes entry for librbd-encrypted RBD volumes
Jul 7, 2026
385fda2
kvm: route the encrypted RBD template import through QemuImg
calvix Aug 17, 2026
b5f780b
kvm: use readable variable names in the encrypted RBD root helpers
calvix Aug 20, 2026
702ff0d
server, kvm: report and require RBD volume encryption separately
calvix Aug 20, 2026
56ea831
kvm: fix RBD exclusive-lock leak that breaks revertSnapshot on Ceph
calvix Aug 10, 2026
064286a
kvm: release RBD handles on every path when cloning a volume from a s…
calvix Aug 10, 2026
bc9f998
kvm: add regression tests for the RBD snapshot handle leak
calvix Aug 10, 2026
3d8eb2a
kvm: extract RBD handle teardown into null-safe helper methods
calvix Aug 10, 2026
fb8fc6a
kvm: extract RBD snapshot unprotect into a helper method
calvix Aug 11, 2026
00f7390
kvm: restore backups into the volume they were taken from
calvix Aug 18, 2026
4b7aa66
kvm: add regression tests for backup restore volume mapping
calvix Aug 18, 2026
f59e63d
kvm: keep create-instance-from-backup working when mapping volumes by…
calvix Aug 21, 2026
e35ce45
nasbackup: keep mount output off stdout, and detect a failed mount
calvix Aug 25, 2026
d820fc8
kvm: do not fail a NAS backup on non-numeric output from nasbackup.sh
calvix Aug 25, 2026
324e829
backup: keep encrypted volumes encrypted in NAS backups
calvix Aug 20, 2026
c5a5a2e
backup: cover the encrypted-volume path in the NAS provider tests
calvix Aug 20, 2026
9abbf32
backup: make the restore path work with encrypted backups
calvix Aug 20, 2026
b2853ad
kvm: restore java.util imports dropped by the backport
calvix Aug 20, 2026
3c71d1e
kvm, backup: adapt the ported backup tests to 4.23
calvix Sep 7, 2026
84ba119
jobs: count VmWork jobs when draining a management server for mainten…
calvix Oct 5, 2026
579141d
agent: move indirect agents to another management server without losi…
calvix Oct 5, 2026
55e13dc
cluster: reopen a peer channel the peer management server has closed
calvix Oct 5, 2026
e961eea
kvm: make the virtio-blk disk controller usable
calvix Sep 24, 2026
a28d5bd
kvm: bind virtio-blk disks to an iothread, and let running SCSI disks…
calvix Sep 24, 2026
f7241d5
kvm: restore a brace lost resolving the cherry-pick conflict in KVMSt…
calvix Oct 1, 2026
d54518a
kvm: drop the virtio-blk checks on running disks, which can never match
calvix Oct 5, 2026
63ed45b
kvm: fill the template's holes before taking its LUKS clone snapshot
calvix Oct 2, 2026
53d399d
vr: release DHCP leases of expunged VMs on redundant routers
calvix Sep 30, 2026
9a936d2
vr: skip the lease wait and dnsmasq restart where they cannot help
calvix Oct 2, 2026
141796a
vr: wait and restart dnsmasq once per run, not once per leftover lease
calvix Oct 4, 2026
1e9186c
backup: keep the scheduler running when a schedule's async job is gone
calvix Oct 3, 2026
19523e3
vr: serve UDP load balancer rules with IPVS
calvix Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions PendingReleaseNotes
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,13 @@ example.ver.1 > example.ver.2:
which can now be attached to Instances. This is to prevent the Secondary
Storage to grow to enormous sizes as Linux Distributions keep growing in
size while a stripped down Linux should fit on a 2.88MB floppy.


4.22.0.0 > 4.23.0.0:
* KVM/Ceph: RBD volumes can now be encrypted at rest using native librbd
LUKS2 (<encryption format='luks2' engine='librbd'>), for both data disks
and root disks. Encryption is transparent to the guest and reuses the
existing CloudStack volume-encryption passphrase handling, so no
additional key store is required. Note: attaching an encrypted RBD volume
to a running Instance requires libvirt >= 10.1.0; booting an Instance from
an encrypted RBD root disk works on older libvirt.
32 changes: 32 additions & 0 deletions agent/src/main/java/com/cloud/agent/Agent.java
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@ private Answer migrateAgentToOtherMS(final MigrateAgentConnectionCommand cmd) {
}
ScheduledExecutorService migrateAgentConnectionService = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("MigrateAgentConnection-Job"));
migrateAgentConnectionService.schedule(() -> {
waitForCommandsToFinish(MIGRATE_AGENT_CONNECTION_MAX_WAIT_SECS);
migrateAgentConnection(cmd.getAvoidMsList());
}, 3, TimeUnit.SECONDS);
migrateAgentConnectionService.shutdown();
Expand All @@ -980,6 +981,37 @@ private Answer migrateAgentToOtherMS(final MigrateAgentConnectionCommand cmd) {
return new MigrateAgentConnectionAnswer(true);
}

private static final int MIGRATE_AGENT_CONNECTION_MAX_WAIT_SECS = 300;

/**
* Answers travel back over the current link, so a command still running or queued when the agent reconnects to
* another management server would lose its answer and fail there. Wait until nothing has run or been queued for two
* consecutive checks; the management server holds new commands for this host while it is Rebalancing.
*/
private void waitForCommandsToFinish(final int maxWaitSecs) {
final long deadline = System.currentTimeMillis() + maxWaitSecs * 1000L;
int idleChecks = 0;
while (idleChecks < 2) {
final int queued = requestHandler instanceof ThreadPoolExecutor ? ((ThreadPoolExecutor)requestHandler).getQueue().size() : 0;
idleChecks = (commandsInProgress.get() == 0 && queued == 0) ? idleChecks + 1 : 0;
if (idleChecks >= 2) {
break;
}
if (System.currentTimeMillis() >= deadline) {
logger.warn("Moving the agent connection with {} commands still in progress and {} queued after {} s",
commandsInProgress.get(), queued, maxWaitSecs);
return;
}
try {
Thread.sleep(1000);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
logger.info("No commands in progress, moving the agent connection");
}

private void migrateAgentConnection(List<String> avoidMsList) {
final String[] msHosts = shell.getHosts();
if (msHosts == null || msHosts.length < 1) {
Expand Down
1 change: 1 addition & 0 deletions api/src/main/java/com/cloud/host/Host.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public static String[] toStrings(Host.Type... types) {

String HOST_UEFI_ENABLE = "host.uefi.enable";
String HOST_VOLUME_ENCRYPTION = "host.volume.encryption";
String HOST_RBD_VOLUME_ENCRYPTION = "host.volume.encryption.rbd";
String HOST_INSTANCE_CONVERSION = "host.instance.conversion";
String HOST_VDDK_SUPPORT = "host.vddk.support";
String HOST_VDDK_LIB_DIR = "vddk.lib.dir";
Expand Down
2 changes: 1 addition & 1 deletion api/src/main/java/com/cloud/storage/Storage.java
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public static enum StoragePoolType {
LVM(false, false, EncryptionSupport.Unsupported), // XenServer local LVM SR
CLVM(true, false, EncryptionSupport.Unsupported),
CLVM_NG(true, false, EncryptionSupport.Hypervisor),
RBD(true, true, EncryptionSupport.Unsupported), // http://libvirt.org/storage.html#StorageBackendRBD
RBD(true, true, EncryptionSupport.Hypervisor), // http://libvirt.org/storage.html#StorageBackendRBD ; encrypted natively by librbd (LUKS2, engine='librbd')
SharedMountPoint(true, true, EncryptionSupport.Hypervisor),
VMFS(true, true, EncryptionSupport.Unsupported), // VMware VMFS storage
PreSetup(true, true, EncryptionSupport.Unsupported), // for XenServer, Storage Pool is set up by customers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@ public Integer getSourcePortEnd() {
}

public String getProtocol() {
// a UDP load balancer needs a UDP firewall rule and must not clash with UDP port forwarding on the same port
if (NetUtils.UDP_PROTO.equals(getLbProtocol())) {
return NetUtils.UDP_PROTO;
}
return NetUtils.TCP_PROTO;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public List<ConfigItem> generateConfig(final NetworkElementCommand cmd) {
final LoadBalancerRule loadBalancerRule = new LoadBalancerRule(configuration, tmpCfgFilePath, tmpCfgFileName, addRules, removeRules, statRules, routerIp);
final LoadBalancerRule.SslCertEntry[] sslCerts = cfgtr.generateSslCertEntries(command);
loadBalancerRule.setSslCerts(sslCerts);
loadBalancerRule.setUdpRules(cfgtr.generateUdpEntries(command));

final List<LoadBalancerRule> rules = new LinkedList<LoadBalancerRule>();
rules.add(loadBalancerRule);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class LoadBalancerRule {
private String tmpCfgFilePath;
private String tmpCfgFileName;
private SslCertEntry[] sslCerts;
private UdpLbEntry[] udpRules;

private String[] addRules;
private String[] removeRules;
Expand Down Expand Up @@ -80,6 +81,50 @@ public String getPassword() {
}
}

/**
* A UDP load balancer rule. The router serves it with IPVS (NAT mode), not haproxy.
*/
public static class UdpLbEntry {
private String sourceIp;
private int sourcePort;
// IPVS scheduler: rr, lc or sh
private String scheduler;
// IPVS persistence timeout in seconds, 0 = none
private int persistence;
// "ip:port" of each backend
private String[] destinations;
// allowed source CIDRs, empty = everyone
private String[] cidrs;

public UdpLbEntry(String sourceIp, int sourcePort, String scheduler, int persistence, String[] destinations, String[] cidrs) {
this.sourceIp = sourceIp;
this.sourcePort = sourcePort;
this.scheduler = scheduler;
this.persistence = persistence;
this.destinations = destinations;
this.cidrs = cidrs;
}

public String getSourceIp() {
return sourceIp;
}
public int getSourcePort() {
return sourcePort;
}
public String getScheduler() {
return scheduler;
}
public int getPersistence() {
return persistence;
}
public String[] getDestinations() {
return destinations;
}
public String[] getCidrs() {
return cidrs;
}
}

public LoadBalancerRule() {
// Empty constructor for (de)serialization
}
Expand Down Expand Up @@ -157,4 +202,12 @@ public SslCertEntry[] getSslCerts() {
public void setSslCerts(final SslCertEntry[] sslCerts) {
this.sslCerts = sslCerts;
}

public UdpLbEntry[] getUdpRules() {
return udpRules;
}

public void setUdpRules(final UdpLbEntry[] udpRules) {
this.udpRules = udpRules;
}
}
75 changes: 74 additions & 1 deletion core/src/main/java/com/cloud/network/HAProxyConfigurator.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.cloud.agent.api.to.LoadBalancerTO.StickinessPolicyTO;
import com.cloud.agent.api.to.PortForwardingRuleTO;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.SslCertEntry;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.UdpLbEntry;
import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import com.cloud.network.rules.LbStickinessMethod.StickinessMethodType;
import com.cloud.utils.Pair;
Expand Down Expand Up @@ -683,7 +684,7 @@ public String[] generateConfiguration(final LoadBalancerConfigCommand lbCmd) {
result.add(blankLine);
boolean has_listener = false;
for (final LoadBalancerTO lbTO : lbCmd.getLoadBalancers()) {
if (lbTO.isRevoked()) {
if (lbTO.isRevoked() || isUdp(lbTO)) {
continue;
}
final List<String> poolRules = getRulesForPool(lbTO, lbCmd);
Expand All @@ -708,6 +709,9 @@ public String[][] generateFwRules(final LoadBalancerConfigCommand lbCmd) {
final Set<String> toStats = new HashSet<String>();

for (final LoadBalancerTO lbTO : lbCmd.getLoadBalancers()) {
if (isUdp(lbTO)) {
continue;
}

final StringBuilder sb = new StringBuilder();
sb.append(lbTO.getSrcIp()).append(":");
Expand Down Expand Up @@ -764,4 +768,73 @@ private void addSslCertEntry(Set<SslCertEntry> sslCertEntries, LoadBalancerTO lb
final SslCertEntry sslCertEntry = new SslCertEntry(name, cert.getCert(), cert.getKey(), cert.getChain(), cert.getPassword());
sslCertEntries.add(sslCertEntry);
}

private static boolean isUdp(final LoadBalancerTO lbTO) {
return NetUtils.UDP_PROTO.equalsIgnoreCase(lbTO.getLbProtocol());
}

@Override
public UdpLbEntry[] generateUdpEntries(final LoadBalancerConfigCommand lbCmd) {
final List<UdpLbEntry> result = new ArrayList<>();
for (final LoadBalancerTO lbTO : lbCmd.getLoadBalancers()) {
if (lbTO.isRevoked() || !isUdp(lbTO)) {
continue;
}
final List<String> destinations = new ArrayList<>();
for (final DestinationTO dest : lbTO.getDestinations()) {
if (!dest.isRevoked()) {
destinations.add(dest.getDestIp() + ":" + dest.getDestPort());
}
}
final String[] cidrs = StringUtils.isBlank(lbTO.getCidrList()) ? new String[0] : lbTO.getCidrList().trim().split("\\s+");
result.add(new UdpLbEntry(lbTO.getSrcIp(), lbTO.getSrcPort(), getIpvsScheduler(lbTO.getAlgorithm()), getIpvsPersistence(lbTO),
destinations.toArray(new String[0]), cidrs));
}
return result.toArray(new UdpLbEntry[0]);
}

protected static String getIpvsScheduler(final String algorithm) {
if ("leastconn".equals(algorithm)) {
return "lc";
}
if ("source".equals(algorithm)) {
return "sh";
}
return "rr";
}

/**
* Source based stickiness becomes IPVS persistence; its "expire" value (default 3h, as for haproxy) is the timeout.
*/
protected static int getIpvsPersistence(final LoadBalancerTO lbTO) {
if (lbTO.getStickinessPolicies() == null) {
return 0;
}
for (final StickinessPolicyTO policy : lbTO.getStickinessPolicies()) {
if (policy == null || !StickinessMethodType.SourceBased.getName().equalsIgnoreCase(policy.getMethodName())) {
continue;
}
String expire = "3h";
for (final Pair<String, String> param : policy.getParams()) {
if ("expire".equalsIgnoreCase(param.first()) && StringUtils.isNotBlank(param.second())) {
expire = param.second().trim();
}
}
return durationToSeconds(expire);
}
return 0;
}

private static int durationToSeconds(final String duration) {
final char unit = duration.charAt(duration.length() - 1);
final int multiplier;
switch (unit) {
case 's': multiplier = 1; break;
case 'm': multiplier = 60; break;
case 'h': multiplier = 3600; break;
case 'd': multiplier = 86400; break;
default: return Integer.parseInt(duration);
}
return Integer.parseInt(duration.substring(0, duration.length() - 1)) * multiplier;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.cloud.agent.api.routing.LoadBalancerConfigCommand;
import com.cloud.agent.api.to.PortForwardingRuleTO;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.SslCertEntry;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.UdpLbEntry;

public interface LoadBalancerConfigurator {
public final static int ADD = 0;
Expand All @@ -37,4 +38,6 @@ public interface LoadBalancerConfigurator {
public String[][] generateFwRules(LoadBalancerConfigCommand lbCmd);

public SslCertEntry[] generateSslCertEntries(LoadBalancerConfigCommand lbCmd);

public UdpLbEntry[] generateUdpEntries(LoadBalancerConfigCommand lbCmd);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class RestoreBackupCommand extends Command {
private String vmName;
Expand All @@ -40,11 +42,38 @@ public class RestoreBackupCommand extends Command {
private Boolean vmExists;
private VirtualMachine.State vmState;
private Integer mountTimeout;
/**
* Passphrase of every encrypted volume being restored, keyed by volume path. The backup itself is
* LUKS-encrypted (see TakeBackupCommand), so the agent needs these both to read the backup and to
* write the data back through the volume's own encryption.
*/
@LogLevel(LogLevel.Log4jLevel.Off)
private Map<String, byte[]> volumePassphrases;

protected RestoreBackupCommand() {
super();
}

public Map<String, byte[]> getVolumePassphrases() {
return volumePassphrases;
}

public void setVolumePassphrases(Map<String, byte[]> volumePassphrases) {
this.volumePassphrases = volumePassphrases;
}

/** Wipes the passphrases once the command has been handled. */
public void clearPassphrases() {
if (volumePassphrases == null) {
return;
}
for (byte[] passphrase : volumePassphrases.values()) {
if (passphrase != null) {
Arrays.fill(passphrase, (byte) 0);
}
}
}

public String getVmName() {
return vmName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import com.cloud.agent.api.LogLevel;
import org.apache.cloudstack.storage.to.PrimaryDataStoreTO;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class TakeBackupCommand extends Command {
private String vmName;
Expand All @@ -35,6 +37,15 @@ public class TakeBackupCommand extends Command {
private Boolean quiesce;
@LogLevel(LogLevel.Log4jLevel.Off)
private String mountOptions;
/**
* Passphrase of every encrypted volume of the VM, keyed by volume path (the uuid the backup
* files are named after). Volumes that are not encrypted are absent. The agent uses these to
* make the backup itself LUKS-encrypted, so an encrypted volume is never written out in the
* clear. Keyed rather than positional because the running-VM path enumerates disks from
* libvirt instead of from {@link #volumePaths}.
*/
@LogLevel(LogLevel.Log4jLevel.Off)
private Map<String, byte[]> volumePassphrases;

// Incremental backup fields (NAS provider; null/empty for legacy full-only callers).
private String mode; // "full" or "incremental"; null => legacy behaviour (script default)
Expand All @@ -53,6 +64,26 @@ public TakeBackupCommand(String vmName, String backupPath) {
this.backupPath = backupPath;
}

public Map<String, byte[]> getVolumePassphrases() {
return volumePassphrases;
}

public void setVolumePassphrases(Map<String, byte[]> volumePassphrases) {
this.volumePassphrases = volumePassphrases;
}

/** Wipes the passphrases once the command has been handled. */
public void clearPassphrases() {
if (volumePassphrases == null) {
return;
}
for (byte[] passphrase : volumePassphrases.values()) {
if (passphrase != null) {
Arrays.fill(passphrase, (byte) 0);
}
}
}

public String getVmName() {
return vmName;
}
Expand Down
Loading
Loading