Image server with disk upload

This commit is contained in:
Abhisar Sinha 2026-01-22 13:28:56 +05:30 committed by Abhishek Kumar
parent 23ecb1f5ce
commit 5389fe60aa
21 changed files with 799 additions and 284 deletions

View File

@ -28,6 +28,7 @@ import org.apache.cloudstack.api.command.admin.AdminCmd;
import org.apache.cloudstack.api.response.BackupResponse;
import org.apache.cloudstack.api.response.ImageTransferResponse;
import org.apache.cloudstack.api.response.VolumeResponse;
import org.apache.cloudstack.backup.ImageTransfer;
import org.apache.cloudstack.backup.IncrementalBackupService;
import org.apache.cloudstack.context.CallContext;
@ -44,7 +45,6 @@ public class CreateImageTransferCmd extends BaseCmd implements AdminCmd {
@Parameter(name = ApiConstants.BACKUP_ID,
type = CommandType.UUID,
entityType = BackupResponse.class,
required = true,
description = "ID of the backup")
private Long backupId;
@ -69,8 +69,8 @@ public class CreateImageTransferCmd extends BaseCmd implements AdminCmd {
return volumeId;
}
public String getDirection() {
return direction;
public ImageTransfer.Direction getDirection() {
return ImageTransfer.Direction.valueOf(direction);
}
@Override

View File

@ -21,6 +21,8 @@ import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.api.InternalIdentity;
public interface ImageTransfer extends ControlledEntity, InternalIdentity {
long getDataCenterId();
public enum Direction {
upload, download
}
@ -33,12 +35,8 @@ public interface ImageTransfer extends ControlledEntity, InternalIdentity {
long getBackupId();
long getVmId();
long getDiskId();
String getDeviceName();
long getHostId();
int getNbdPort();

View File

@ -22,7 +22,6 @@ import com.cloud.agent.api.Answer;
public class CreateImageTransferAnswer extends Answer {
private String imageTransferId;
private String transferUrl;
private String phase;
public CreateImageTransferAnswer() {
}
@ -32,11 +31,10 @@ public class CreateImageTransferAnswer extends Answer {
}
public CreateImageTransferAnswer(CreateImageTransferCommand cmd, boolean success, String details,
String imageTransferId, String transferUrl, String phase) {
String imageTransferId, String transferUrl) {
super(cmd, success, details);
this.imageTransferId = imageTransferId;
this.transferUrl = transferUrl;
this.phase = phase;
}
public String getImageTransferId() {
@ -55,11 +53,4 @@ public class CreateImageTransferAnswer extends Answer {
this.transferUrl = transferUrl;
}
public String getPhase() {
return phase;
}
public void setPhase(String phase) {
this.phase = phase;
}
}

View File

@ -22,21 +22,25 @@ import com.cloud.agent.api.Command;
public class CreateImageTransferCommand extends Command {
private String transferId;
private String hostIpAddress;
private String deviceName;
private String exportName;
private String volumePath;
private int nbdPort;
private String direction;
public CreateImageTransferCommand() {
}
public CreateImageTransferCommand(Long vmId, String transferId, String hostIpAddress, Long backupId, Long diskId, String deviceName, int nbdPort) {
public CreateImageTransferCommand(String transferId, String hostIpAddress, String exportName, String volumePath, int nbdPort, String direction) {
this.transferId = transferId;
this.hostIpAddress = hostIpAddress;
this.deviceName = deviceName;
this.exportName = exportName;
this.volumePath = volumePath;
this.nbdPort = nbdPort;
this.direction = direction;
}
public String getDeviceName() {
return deviceName;
public String getExportName() {
return exportName;
}
public int getNbdPort() {
@ -55,4 +59,12 @@ public class CreateImageTransferCommand extends Command {
public boolean executeInSequence() {
return true;
}
public String getVolumePath() {
return volumePath;
}
public String getDirection() {
return direction;
}
}

View File

@ -21,18 +21,30 @@ import com.cloud.agent.api.Command;
public class FinalizeImageTransferCommand extends Command {
private String transferId;
private String direction;
private int nbdPort;
public FinalizeImageTransferCommand() {
}
public FinalizeImageTransferCommand(String transferId) {
public FinalizeImageTransferCommand(String transferId, String direction, int nbdPort) {
this.transferId = transferId;
this.direction = direction;
this.nbdPort = nbdPort;
}
public String getTransferId() {
return transferId;
}
public int getNbdPort() {
return nbdPort;
}
public String getDirection() {
return direction;
}
@Override
public boolean executeInSequence() {
return true;

View File

@ -17,13 +17,11 @@
package org.apache.cloudstack.backup;
import java.util.Map;
import com.cloud.agent.api.Answer;
public class StartBackupAnswer extends Answer {
private Long checkpointCreateTime;
private Map<Long, String> deviceMappings; // volumeId -> device name (vda, vdb, etc.)
private Boolean isIncremental;
public StartBackupAnswer() {
}
@ -32,11 +30,9 @@ public class StartBackupAnswer extends Answer {
super(cmd, success, details);
}
public StartBackupAnswer(StartBackupCommand cmd, boolean success, String details,
Long checkpointCreateTime, Map<Long, String> deviceMappings) {
public StartBackupAnswer(StartBackupCommand cmd, boolean success, String details, Long checkpointCreateTime) {
super(cmd, success, details);
this.checkpointCreateTime = checkpointCreateTime;
this.deviceMappings = deviceMappings;
}
public Long getCheckpointCreateTime() {
@ -47,11 +43,11 @@ public class StartBackupAnswer extends Answer {
this.checkpointCreateTime = checkpointCreateTime;
}
public Map<Long, String> getDeviceMappings() {
return deviceMappings;
public Boolean getIncremental() {
return isIncremental;
}
public void setDeviceMappings(Map<Long, String> deviceMappings) {
this.deviceMappings = deviceMappings;
public void setIncremental(Boolean incremental) {
isIncremental = incremental;
}
}

View File

@ -23,20 +23,18 @@ import com.cloud.agent.api.Command;
public class StartBackupCommand extends Command {
private String vmName;
private Long vmId;
private String toCheckpointId;
private String fromCheckpointId;
private int nbdPort;
private Map<Long, String> diskVolumePaths; // volumeId -> path mapping
private Map<String, String> diskVolumePaths; // volumeId -> path mapping
private String hostIpAddress;
public StartBackupCommand() {
}
public StartBackupCommand(String vmName, Long vmId, String toCheckpointId, String fromCheckpointId,
int nbdPort, Map<Long, String> diskVolumePaths, String hostIpAddress) {
public StartBackupCommand(String vmName, String toCheckpointId, String fromCheckpointId,
int nbdPort, Map<String, String> diskVolumePaths, String hostIpAddress) {
this.vmName = vmName;
this.vmId = vmId;
this.toCheckpointId = toCheckpointId;
this.fromCheckpointId = fromCheckpointId;
this.nbdPort = nbdPort;
@ -48,10 +46,6 @@ public class StartBackupCommand extends Command {
return vmName;
}
public Long getVmId() {
return vmId;
}
public String getToCheckpointId() {
return toCheckpointId;
}
@ -64,7 +58,7 @@ public class StartBackupCommand extends Command {
return nbdPort;
}
public Map<Long, String> getDiskVolumePaths() {
public Map<String, String> getDiskVolumePaths() {
return diskVolumePaths;
}

View File

@ -0,0 +1,56 @@
//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
//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 org.apache.cloudstack.backup;
import com.cloud.agent.api.Answer;
public class StartNBDServerAnswer extends Answer {
private String imageTransferId;
private String transferUrl;
public StartNBDServerAnswer() {
}
public StartNBDServerAnswer(StartNBDServerCommand cmd, boolean success, String details) {
super(cmd, success, details);
}
public StartNBDServerAnswer(StartNBDServerCommand cmd, boolean success, String details,
String imageTransferId, String transferUrl) {
super(cmd, success, details);
this.imageTransferId = imageTransferId;
this.transferUrl = transferUrl;
}
public String getImageTransferId() {
return imageTransferId;
}
public void setImageTransferId(String imageTransferId) {
this.imageTransferId = imageTransferId;
}
public String getTransferUrl() {
return transferUrl;
}
public void setTransferUrl(String transferUrl) {
this.transferUrl = transferUrl;
}
}

View File

@ -0,0 +1,70 @@
//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
//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 org.apache.cloudstack.backup;
import com.cloud.agent.api.Command;
public class StartNBDServerCommand extends Command {
private String transferId;
private String hostIpAddress;
private String exportName;
private String volumePath;
private int nbdPort;
private String direction;
public StartNBDServerCommand() {
}
public StartNBDServerCommand(String transferId, String hostIpAddress, String exportName, String volumePath, int nbdPort, String direction) {
this.transferId = transferId;
this.hostIpAddress = hostIpAddress;
this.exportName = exportName;
this.volumePath = volumePath;
this.nbdPort = nbdPort;
this.direction = direction;
}
public String getExportName() {
return exportName;
}
public int getNbdPort() {
return nbdPort;
}
public String getHostIpAddress() {
return hostIpAddress;
}
public String getTransferId() {
return transferId;
}
@Override
public boolean executeInSequence() {
return true;
}
public String getVolumePath() {
return volumePath;
}
public String getDirection() {
return direction;
}
}

View File

@ -0,0 +1,52 @@
//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
//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 org.apache.cloudstack.backup;
import com.cloud.agent.api.Command;
public class StopNBDServerCommand extends Command {
private String transferId;
private String direction;
private int nbdPort;
public StopNBDServerCommand() {
}
public StopNBDServerCommand(String transferId, String direction, int nbdPort) {
this.transferId = transferId;
this.direction = direction;
this.nbdPort = nbdPort;
}
public String getTransferId() {
return transferId;
}
public int getNbdPort() {
return nbdPort;
}
public String getDirection() {
return direction;
}
@Override
public boolean executeInSequence() {
return true;
}
}

View File

@ -45,15 +45,9 @@ public class ImageTransferVO implements ImageTransfer {
@Column(name = "backup_id")
private long backupId;
@Column(name = "vm_id")
private long vmId;
@Column(name = "disk_id")
private long diskId;
@Column(name = "device_name")
private String deviceName;
@Column(name = "host_id")
private long hostId;
@ -80,6 +74,9 @@ public class ImageTransferVO implements ImageTransfer {
@Column(name = "domain_id")
Long domainId;
@Column(name = "data_center_id")
Long dataCenterId;
@Column(name = "created")
@Temporal(value = TemporalType.TIMESTAMP)
private Date created;
@ -95,18 +92,17 @@ public class ImageTransferVO implements ImageTransfer {
public ImageTransferVO() {
}
public ImageTransferVO(String uuid, long backupId, long vmId, long diskId, String deviceName, long hostId, int nbdPort, Phase phase, Direction direction, Long accountId, Long domainId) {
public ImageTransferVO(String uuid, Long backupId, long diskId, long hostId, int nbdPort, Phase phase, Direction direction, Long accountId, Long domainId, Long dataCenterId) {
this.uuid = uuid;
this.backupId = backupId;
this.vmId = vmId;
this.diskId = diskId;
this.deviceName = deviceName;
this.hostId = hostId;
this.nbdPort = nbdPort;
this.phase = phase;
this.direction = direction;
this.accountId = accountId;
this.domainId = domainId;
this.dataCenterId = dataCenterId;
this.created = new Date();
}
@ -129,15 +125,6 @@ public class ImageTransferVO implements ImageTransfer {
this.backupId = backupId;
}
@Override
public long getVmId() {
return vmId;
}
public void setVmId(long vmId) {
this.vmId = vmId;
}
@Override
public long getDiskId() {
return diskId;
@ -147,15 +134,6 @@ public class ImageTransferVO implements ImageTransfer {
this.diskId = diskId;
}
@Override
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
@Override
public long getHostId() {
return hostId;
@ -231,6 +209,11 @@ public class ImageTransferVO implements ImageTransfer {
return accountId;
}
@Override
public long getDataCenterId() {
return dataCenterId;
}
public Date getCreated() {
return created;
}

View File

@ -25,6 +25,6 @@ import com.cloud.utils.db.GenericDao;
public interface ImageTransferDao extends GenericDao<ImageTransferVO, Long> {
List<ImageTransferVO> listByBackupId(Long backupId);
List<ImageTransferVO> listByVmId(Long vmId);
ImageTransferVO findByUuid(String uuid);
ImageTransferVO findByNbdPort(int port);
}

View File

@ -32,8 +32,8 @@ import com.cloud.utils.db.SearchCriteria;
public class ImageTransferDaoImpl extends GenericDaoBase<ImageTransferVO, Long> implements ImageTransferDao {
private SearchBuilder<ImageTransferVO> backupIdSearch;
private SearchBuilder<ImageTransferVO> vmIdSearch;
private SearchBuilder<ImageTransferVO> uuidSearch;
private SearchBuilder<ImageTransferVO> nbdPortSearch;
public ImageTransferDaoImpl() {
}
@ -44,13 +44,13 @@ public class ImageTransferDaoImpl extends GenericDaoBase<ImageTransferVO, Long>
backupIdSearch.and("backupId", backupIdSearch.entity().getBackupId(), SearchCriteria.Op.EQ);
backupIdSearch.done();
vmIdSearch = createSearchBuilder();
vmIdSearch.and("vmId", vmIdSearch.entity().getVmId(), SearchCriteria.Op.EQ);
vmIdSearch.done();
uuidSearch = createSearchBuilder();
uuidSearch.and("uuid", uuidSearch.entity().getUuid(), SearchCriteria.Op.EQ);
uuidSearch.done();
nbdPortSearch = createSearchBuilder();
nbdPortSearch.and("nbdPort", nbdPortSearch.entity().getNbdPort(), SearchCriteria.Op.EQ);
nbdPortSearch.done();
}
@Override
@ -60,17 +60,17 @@ public class ImageTransferDaoImpl extends GenericDaoBase<ImageTransferVO, Long>
return listBy(sc);
}
@Override
public List<ImageTransferVO> listByVmId(Long vmId) {
SearchCriteria<ImageTransferVO> sc = vmIdSearch.create();
sc.setParameters("vmId", vmId);
return listBy(sc);
}
@Override
public ImageTransferVO findByUuid(String uuid) {
SearchCriteria<ImageTransferVO> sc = uuidSearch.create();
sc.setParameters("uuid", uuid);
return findOneBy(sc);
}
@Override
public ImageTransferVO findByNbdPort(int port) {
SearchCriteria<ImageTransferVO> sc = nbdPortSearch.create();
sc.setParameters("nbdPort", port);
return findOneBy(sc);
}
}

View File

@ -131,14 +131,13 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vm_instance', 'active_checkpoint_cre
-- Create image_transfer table for per-disk image transfers
CREATE TABLE IF NOT EXISTS `cloud`.`image_transfer`(
`id` bigint unsigned NOT NULL auto_increment COMMENT 'id',
`uuid` varchar(40) NOT NULL COMMENT 'uuid',
`id` bigint unsigned NOT NULL auto_increment COMMENT 'id',
`uuid` varchar(40) NOT NULL COMMENT 'uuid',
`account_id` bigint unsigned NOT NULL COMMENT 'Account ID',
`domain_id` bigint unsigned NOT NULL COMMENT 'Domain ID',
`backup_id` bigint unsigned NOT NULL COMMENT 'Backup ID',
`vm_id` bigint unsigned NOT NULL COMMENT 'VM ID',
`data_center_id` bigint unsigned NOT NULL COMMENT 'Data Center ID',
`backup_id` bigint unsigned COMMENT 'Backup ID',
`disk_id` bigint unsigned NOT NULL COMMENT 'Disk/Volume ID',
`device_name` varchar(10) NOT NULL COMMENT 'Device name (vda, vdb, etc)',
`host_id` bigint unsigned NOT NULL COMMENT 'Host ID',
`nbd_port` int NOT NULL COMMENT 'NBD port',
`transfer_url` varchar(255) COMMENT 'ImageIO transfer URL',
@ -151,9 +150,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`image_transfer`(
PRIMARY KEY (`id`),
UNIQUE KEY `uuid` (`uuid`),
CONSTRAINT `fk_image_transfer__backup_id` FOREIGN KEY (`backup_id`) REFERENCES `backups`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_image_transfer__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `vm_instance`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_image_transfer__disk_id` FOREIGN KEY (`disk_id`) REFERENCES `volumes`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_image_transfer__host_id` FOREIGN KEY (`host_id`) REFERENCES `host`(`id`) ON DELETE CASCADE,
INDEX `i_image_transfer__backup_id`(`backup_id`),
INDEX `i_image_transfer__vm_id`(`vm_id`)
INDEX `i_image_transfer__backup_id`(`backup_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -19,8 +19,8 @@ package com.cloud.hypervisor.kvm.resource.wrapper;
import org.apache.cloudstack.backup.CreateImageTransferAnswer;
import org.apache.cloudstack.backup.CreateImageTransferCommand;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
@ -31,27 +31,31 @@ import com.cloud.resource.ResourceWrapper;
public class LibvirtCreateImageTransferCommandWrapper extends CommandWrapper<CreateImageTransferCommand, Answer, LibvirtComputingResource> {
protected Logger logger = LogManager.getLogger(getClass());
@Override
public Answer execute(CreateImageTransferCommand cmd, LibvirtComputingResource resource) {
String deviceName = cmd.getDeviceName();
private CreateImageTransferAnswer handleUpload(CreateImageTransferCommand cmd) {
return new CreateImageTransferAnswer(cmd, false, "Image Upload is not handled by KVM agent");
}
private CreateImageTransferAnswer handleDownload(CreateImageTransferCommand cmd) {
String exportName = cmd.getExportName();
int nbdPort = cmd.getNbdPort();
try {
// POC: ImageIO interaction is stubbed out
// In production, this would:
// 1. Register NBD endpoint nbd://127.0.0.1:{nbdPort}/{deviceName} with ImageIO
// 2. Create transfer object in ImageIO
// 3. Get signed ticket and transfer URL
String hostIpAddress = cmd.getHostIpAddress();
String transferUrl = String.format("nbd://%s:%d/%s", hostIpAddress, nbdPort, deviceName);
String phase = "initializing";
String transferUrl = String.format("nbd://%s:%d/%s", hostIpAddress, nbdPort, exportName);
return new CreateImageTransferAnswer(cmd, true, "Image transfer created (stub)",
cmd.getTransferId(), transferUrl, phase);
return new CreateImageTransferAnswer(cmd, true, "Image transfer created for download",
cmd.getTransferId(), transferUrl);
} catch (Exception e) {
return new CreateImageTransferAnswer(cmd, false, "Error creating image transfer: " + e.getMessage());
}
}
@Override
public Answer execute(CreateImageTransferCommand cmd, LibvirtComputingResource resource) {
if (cmd.getDirection().equals("download")) {
return handleDownload(cmd);
} else {
return handleUpload(cmd);
}
}
}

View File

@ -19,7 +19,6 @@ package com.cloud.hypervisor.kvm.resource.wrapper;
import java.io.File;
import java.io.FileWriter;
import java.util.HashMap;
import java.util.Map;
import org.apache.cloudstack.backup.StartBackupAnswer;
@ -95,11 +94,7 @@ public class LibvirtStartBackupCommandWrapper extends CommandWrapper<StartBackup
// Get checkpoint creation time - using current time for POC
long checkpointCreateTime = System.currentTimeMillis();
// Build device mappings from domblklist
Map<Long, String> deviceMappings = getDeviceMappings(vmName, cmd.getDiskVolumePaths(), resource);
return new StartBackupAnswer(cmd, true, "Backup started successfully",
checkpointCreateTime, deviceMappings);
return new StartBackupAnswer(cmd, true, "Backup started successfully", checkpointCreateTime);
} catch (Exception e) {
return new StartBackupAnswer(cmd, false, "Error starting backup: " + e.getMessage());
@ -118,13 +113,13 @@ public class LibvirtStartBackupCommandWrapper extends CommandWrapper<StartBackup
xml.append(" <disks>\n");
// Add disk entries - simplified for POC
Map<Long, String> diskPaths = cmd.getDiskVolumePaths();
Map<String, String> diskPaths = cmd.getDiskVolumePaths();
int diskIndex = 0;
for (Map.Entry<Long, String> entry : diskPaths.entrySet()) {
for (Map.Entry<String, String> entry : diskPaths.entrySet()) {
String deviceName = "vd" + (char)('a' + diskIndex);
String scratchFile = "/var/tmp/scratch-" + entry.getKey() + ".qcow2";
xml.append(" <disk name=\"").append(deviceName).append("\" type=\"file\" exportname=\"")
.append(deviceName).append("\">\n");
.append(entry.getKey()).append("\">\n");
xml.append(" <scratch file=\"").append(scratchFile).append("\"/>\n");
xml.append(" </disk>\n");
diskIndex++;
@ -141,19 +136,4 @@ public class LibvirtStartBackupCommandWrapper extends CommandWrapper<StartBackup
" <name>" + checkpointId + "</name>\n" +
"</domaincheckpoint>";
}
private Map<Long, String> getDeviceMappings(String vmName, Map<Long, String> diskPaths,
LibvirtComputingResource resource) {
Map<Long, String> mappings = new HashMap<>();
// Simplified for POC - map volumeIds to device names in order
int diskIndex = 0;
for (Long volumeId : diskPaths.keySet()) {
String deviceName = "vd" + (char)('a' + diskIndex);
mappings.put(volumeId, deviceName);
diskIndex++;
}
return mappings;
}
}

View File

@ -0,0 +1,130 @@
//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
//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.hypervisor.kvm.resource.wrapper;
import org.apache.cloudstack.backup.StartNBDServerAnswer;
import org.apache.cloudstack.backup.StartNBDServerCommand;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.script.Script;
@ResourceWrapper(handles = StartNBDServerCommand.class)
public class LibvirtStartNBDServerCommandWrapper extends CommandWrapper<StartNBDServerCommand, Answer, LibvirtComputingResource> {
protected Logger logger = LogManager.getLogger(getClass());
private StartNBDServerAnswer handleUpload(StartNBDServerCommand cmd) {
String volumePath = cmd.getVolumePath();
int nbdPort = cmd.getNbdPort();
String hostIpAddress = cmd.getHostIpAddress();
String exportName = cmd.getExportName();
String transferId = cmd.getTransferId();
if (volumePath == null || volumePath.isEmpty()) {
return new StartNBDServerAnswer(cmd, false, "Volume path is required for upload");
}
if (exportName == null || exportName.isEmpty()) {
return new StartNBDServerAnswer(cmd, false, "Export name is required for upload");
}
if (hostIpAddress == null || hostIpAddress.isEmpty()) {
return new StartNBDServerAnswer(cmd, false, "Host IP address is required for upload");
}
String unitName = String.format("qemu-nbd-%d", nbdPort);
Script checkScript = new Script("/bin/bash", logger);
checkScript.add("-c");
checkScript.add(String.format("systemctl is-active --quiet %s", unitName));
String checkResult = checkScript.execute();
if (checkResult == null) {
return new StartNBDServerAnswer(cmd, false, "A qemu-nbd service is already running on the port.");
}
String systemdRunCmd = String.format(
"systemd-run --unit=%s --property=Restart=no qemu-nbd --export-name %s --bind %s --port %d --persistent %s",
unitName, exportName, hostIpAddress, nbdPort, volumePath
);
Script startScript = new Script("/bin/bash", logger);
startScript.add("-c");
startScript.add(systemdRunCmd);
String startResult = startScript.execute();
if (startResult != null) {
logger.error(String.format("Failed to start qemu-nbd service: %s", startResult));
return new StartNBDServerAnswer(cmd, false, "Failed to start qemu-nbd service: " + startResult);
}
// Wait with timeout until the service is up
int maxWaitSeconds = 10;
int pollIntervalMs = 1000;
int maxAttempts = (maxWaitSeconds * 1000) / pollIntervalMs;
boolean serviceActive = false;
for (int attempt = 0; attempt < maxAttempts; attempt++) {
Script verifyScript = new Script("/bin/bash", logger);
verifyScript.add("-c");
verifyScript.add(String.format("systemctl is-active --quiet %s", unitName));
String verifyResult = verifyScript.execute();
if (verifyResult == null) {
serviceActive = true;
logger.info(String.format("qemu-nbd service %s is now active (attempt %d)", unitName, attempt + 1));
break;
}
try {
Thread.sleep(pollIntervalMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new StartNBDServerAnswer(cmd, false, "Interrupted while waiting for qemu-nbd service to start");
}
}
if (!serviceActive) {
logger.error(String.format("qemu-nbd service %s failed to become active within %d seconds", unitName, maxWaitSeconds));
return new StartNBDServerAnswer(cmd, false,
String.format("qemu-nbd service failed to start within %d seconds", maxWaitSeconds));
}
String transferUrl = String.format("nbd://%s:%d/%s", hostIpAddress, nbdPort, exportName);
return new StartNBDServerAnswer(cmd, true, "qemu-nbd service started for upload",
transferId, transferUrl);
}
private StartNBDServerAnswer handleDownload(StartNBDServerCommand cmd) {
String exportName = cmd.getExportName();
int nbdPort = cmd.getNbdPort();
String hostIpAddress = cmd.getHostIpAddress();
String transferUrl = String.format("nbd://%s:%d/%s", hostIpAddress, nbdPort, exportName);
return new StartNBDServerAnswer(cmd, true, "qemu-nbd service started for download",
cmd.getTransferId(), transferUrl);
}
@Override
public Answer execute(StartNBDServerCommand cmd, LibvirtComputingResource resource) {
if (cmd.getDirection().equals("download")) {
return handleDownload(cmd);
} else {
return handleUpload(cmd);
}
}
}

View File

@ -0,0 +1,86 @@
//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
//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.hypervisor.kvm.resource.wrapper;
import org.apache.cloudstack.backup.StopNBDServerCommand;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.script.Script;
@ResourceWrapper(handles = StopNBDServerCommand.class)
public class LibvirtStopNBDServerCommandWrapper extends CommandWrapper<StopNBDServerCommand, 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 Answer handleUpload(StopNBDServerCommand cmd) {
try {
int nbdPort = cmd.getNbdPort();
String unitName = String.format("qemu-nbd-%d", nbdPort);
// Check if the service is running
Script checkScript = new Script("/bin/bash", logger);
checkScript.add("-c");
checkScript.add(String.format("systemctl is-active --quiet %s", unitName));
String checkResult = checkScript.execute();
if (checkResult != null) {
// Service is not running, but still reset-failed to clear any stale state
logger.info(String.format("qemu-nbd service %s is not running, resetting failed state", unitName));
resetService(unitName);
return new Answer(cmd, true, "Image transfer finalized");
}
// Stop the systemd service
Script stopScript = new Script("/bin/bash", logger);
stopScript.add("-c");
stopScript.add(String.format("systemctl stop %s", unitName));
stopScript.execute();
resetService(unitName);
return new Answer(cmd, true, "Image transfer finalized");
} catch (Exception e) {
logger.error("Error finalizing image transfer for upload", e);
return new Answer(cmd, false, "Error finalizing image transfer: " + e.getMessage());
}
}
private Answer handleDownload(StopNBDServerCommand cmd) {
return new Answer(cmd, true, "Image transfer finalized");
}
@Override
public Answer execute(StopNBDServerCommand cmd, LibvirtComputingResource resource) {
if (cmd.getDirection().equals("download")) {
return handleDownload(cmd);
} else {
return handleUpload(cmd);
}
}
}

View File

@ -43,6 +43,8 @@ import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.apache.cloudstack.backup.dao.ImageTransferDao;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.commons.collections.CollectionUtils;
import org.joda.time.DateTime;
import org.springframework.stereotype.Component;
@ -52,8 +54,11 @@ import com.cloud.agent.api.Answer;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.storage.ScopeType;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.exception.CloudRuntimeException;
@ -85,6 +90,9 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
@Inject
private HostDao hostDao;
@Inject
private PrimaryDataStoreDao primaryDataStoreDao;
@Inject
EndPointSelector _epSelector;
@ -110,7 +118,6 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
public BackupResponse startBackup(StartBackupCmd cmd) {
Long vmId = cmd.getVmId();
// Get VM
VMInstanceVO vm = vmInstanceDao.findById(vmId);
if (vm == null) {
throw new CloudRuntimeException("VM not found: " + vmId);
@ -120,7 +127,6 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
throw new CloudRuntimeException("VM must be running to start backup");
}
// Check if backup already in progress
Backup existingBackup = backupDao.findByVmId(vmId);
if (existingBackup != null && existingBackup.getStatus() == Backup.Status.BackingUp) {
throw new CloudRuntimeException("Backup already in progress for VM: " + vmId);
@ -128,45 +134,39 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
boolean dummyOffering = isDummyOffering(vm.getBackupOfferingId());
// Create backup record
BackupVO backup = new BackupVO();
backup.setVmId(vmId);
backup.setName(vmId + "-" + DateTime.now());
backup.setAccountId(vm.getAccountId());
backup.setDomainId(vm.getDomainId());
// todo: set to Increment if it is incremental backup
backup.setType("FULL");
backup.setZoneId(vm.getDataCenterId());
backup.setStatus(Backup.Status.BackingUp);
backup.setBackupOfferingId(vm.getBackupOfferingId());
backup.setDate(new Date());
// Generate checkpoint IDs
String toCheckpointId = "ckp-" + UUID.randomUUID().toString().substring(0, 8);
String fromCheckpointId = vm.getActiveCheckpointId(); // null for first full backup
String fromCheckpointId = vm.getActiveCheckpointId();
backup.setToCheckpointId(toCheckpointId);
backup.setFromCheckpointId(fromCheckpointId);
// Allocate NBD port
int nbdPort = allocateNbdPort();
backup.setNbdPort(nbdPort);
backup.setHostId(vm.getHostId());
// Will be changed later if incremental was done
backup.setType("FULL");
// Persist backup record
backup = backupDao.persist(backup);
// Get disk volume paths
List<? extends Volume> volumes = volumeDao.findByInstance(vmId);
Map<Long, String> diskVolumePaths = new HashMap<>();
List<VolumeVO> volumes = volumeDao.findByInstance(vmId);
Map<String, String> diskVolumePaths = new HashMap<>();
for (Volume vol : volumes) {
diskVolumePaths.put(vol.getId(), vol.getPath());
diskVolumePaths.put(vol.getUuid(), vol.getPath());
}
Host host = hostDao.findById(vm.getHostId());
StartBackupCommand startCmd = new StartBackupCommand(
vm.getInstanceName(),
vmId,
toCheckpointId,
fromCheckpointId,
nbdPort,
@ -178,7 +178,7 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
StartBackupAnswer answer;
if (dummyOffering) {
answer = new StartBackupAnswer(startCmd, true, "Dummy answer", System.currentTimeMillis(), diskVolumePaths);
answer = new StartBackupAnswer(startCmd, true, "Dummy answer", System.currentTimeMillis());
} else {
answer = (StartBackupAnswer) agentManager.send(vm.getHostId(), startCmd);
}
@ -190,9 +190,12 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
// Update backup with checkpoint creation time
backup.setCheckpointCreateTime(answer.getCheckpointCreateTime());
if (Boolean.TRUE.equals(answer.getIncremental())) {
// todo: set it in the backend
backup.setType("Incremental");
}
backupDao.update(backup.getId(), backup);
// Return response
BackupResponse response = new BackupResponse();
response.setId(backup.getUuid());
response.setVmId(vm.getUuid());
@ -270,48 +273,35 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
}
}
@Override
public ImageTransferResponse createImageTransfer(CreateImageTransferCmd cmd) {
private ImageTransferVO createDownloadImageTransfer(CreateImageTransferCmd cmd) {
Long backupId = cmd.getBackupId();
Long volumeId = cmd.getVolumeId();
BackupVO backup = backupDao.findById(backupId);
if (backup == null) {
throw new CloudRuntimeException("Backup not found: " + backupId);
}
boolean dummyOffering = isDummyOffering(backup.getBackupOfferingId());
Volume volume = volumeDao.findById(volumeId);
if (volume == null) {
throw new CloudRuntimeException("Volume not found: " + volumeId);
}
VMInstanceVO vm = vmInstanceDao.findById(backup.getVmId());
if (vm == null) {
throw new CloudRuntimeException("VM not found: " + backup.getVmId());
}
boolean dummyOffering = isDummyOffering(vm.getBackupOfferingId());
// Resolve device name (simplified for POC)
List<? extends Volume> volumes = volumeDao.findByInstance(backup.getVmId());
String deviceName = resolveDeviceName(volumes, volumeId);
String transferId = UUID.randomUUID().toString();
Host host = hostDao.findById(backup.getHostId());
// Create CreateImageTransferCommand
CreateImageTransferCommand transferCmd = new CreateImageTransferCommand(
backup.getVmId(),
transferId,
host.getPrivateIpAddress(),
backupId,
volumeId,
deviceName,
backup.getNbdPort()
transferId,
host.getPrivateIpAddress(),
volume.getUuid(),
null,
backup.getNbdPort(),
cmd.getDirection().toString()
);
try {
CreateImageTransferAnswer answer;
if (dummyOffering) {
answer = new CreateImageTransferAnswer(transferCmd, true, "Dummy answer", "image-transfer-id", "nbd://127.0.0.1:10809/vda", "initializing");
answer = new CreateImageTransferAnswer(transferCmd, true, "Dummy answer", "image-transfer-id", "nbd://127.0.0.1:10809/vda");
} else if (DATAPLANE_PROXY_MODE) {
EndPoint ssvm = _epSelector.findSsvm(backup.getZoneId());
answer = (CreateImageTransferAnswer) ssvm.sendMessage(transferCmd);
@ -323,55 +313,131 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
throw new CloudRuntimeException("Failed to create image transfer: " + answer.getDetails());
}
// Create ImageTransfer record
ImageTransferVO imageTransfer = new ImageTransferVO(
transferId,
backupId,
backup.getVmId(),
volumeId,
deviceName,
backup.getHostId(),
backup.getNbdPort(),
ImageTransferVO.Phase.initializing,
ImageTransfer.Direction.valueOf(cmd.getDirection()),
backup.getAccountId(),
backup.getDomainId()
transferId,
backupId,
volumeId,
backup.getHostId(),
backup.getNbdPort(),
ImageTransferVO.Phase.transferring,
ImageTransfer.Direction.download,
backup.getAccountId(),
backup.getDomainId(),
backup.getZoneId()
);
imageTransfer.setTransferUrl(answer.getTransferUrl());
imageTransfer.setSignedTicketId(answer.getImageTransferId());
imageTransfer = imageTransferDao.persist(imageTransfer);
// Return response
ImageTransferResponse response = new ImageTransferResponse();
response.setId(imageTransfer.getUuid());
response.setBackupId(backup.getUuid());
response.setVmId(vm.getUuid());
response.setDiskId(volume.getUuid());
response.setDeviceName(deviceName);
response.setTransferUrl(answer.getTransferUrl());
response.setPhase(ImageTransferVO.Phase.initializing.toString());
response.setDirection(imageTransfer.getDirection().toString());
response.setCreated(imageTransfer.getCreated());
return response;
return imageTransfer;
} catch (AgentUnavailableException | OperationTimedoutException e) {
throw new CloudRuntimeException("Failed to communicate with agent: " + e.getMessage(), e);
}
}
@Override
public boolean finalizeImageTransfer(FinalizeImageTransferCmd cmd) {
Long imageTransferId = cmd.getImageTransferId();
private HostVO getFirstHostFromStoragePool(StoragePoolVO storagePoolVO) {
List<HostVO> hosts = null;
if (storagePoolVO.getScope().equals(ScopeType.CLUSTER)) {
hosts = hostDao.findByClusterId(storagePoolVO.getClusterId());
ImageTransferVO imageTransfer = imageTransferDao.findById(imageTransferId);
if (imageTransfer == null) {
throw new CloudRuntimeException("Image transfer not found: " + imageTransferId);
} else if (storagePoolVO.getScope().equals(ScopeType.ZONE)) {
hosts = hostDao.findByDataCenterId(storagePoolVO.getDataCenterId());
}
return hosts.get(0);
}
private ImageTransferVO createUploadImageTransfer(CreateImageTransferCmd cmd) {
String transferId = UUID.randomUUID().toString();
int nbdPort = allocateNbdPort();
VolumeVO volume = volumeDao.findById(cmd.getVolumeId());
Long poolId = volume.getPoolId();
StoragePoolVO storagePoolVO = primaryDataStoreDao.findById(poolId);
Host host = getFirstHostFromStoragePool(storagePoolVO);
StartNBDServerAnswer nbdServerAnswer;
StartNBDServerCommand nbdServerCmd = new StartNBDServerCommand(
transferId,
host.getPrivateIpAddress(),
volume.getUuid(),
volume.getPath(),
nbdPort,
cmd.getDirection().toString()
);
try {
nbdServerAnswer = (StartNBDServerAnswer) agentManager.send(host.getId(), nbdServerCmd);
} catch (AgentUnavailableException | OperationTimedoutException e) {
throw new CloudRuntimeException("Failed to communicate with agent: " + e.getMessage(), e);
}
if (!nbdServerAnswer.getResult()) {
throw new CloudRuntimeException("Failed to start the NBD server");
}
CreateImageTransferAnswer transferAnswer;
CreateImageTransferCommand transferCmd = new CreateImageTransferCommand(
transferId,
host.getPrivateIpAddress(),
volume.getUuid(),
volume.getPath(),
nbdPort,
cmd.getDirection().toString()
);
EndPoint ssvm = _epSelector.findSsvm(volume.getDataCenterId());
transferAnswer = (CreateImageTransferAnswer) ssvm.sendMessage(transferCmd);
if (!transferAnswer.getResult()) {
StopNBDServerCommand stopNbdServerCommand = new StopNBDServerCommand(transferId, cmd.getDirection().toString(), nbdPort);
throw new CloudRuntimeException("Failed to create image transfer: " + transferAnswer.getDetails());
}
ImageTransferVO imageTransfer = new ImageTransferVO(
transferId,
null,
volume.getId(),
host.getId(),
nbdPort,
ImageTransferVO.Phase.initializing,
ImageTransfer.Direction.upload,
volume.getAccountId(),
volume.getDomainId(),
volume.getDataCenterId()
);
imageTransfer.setTransferUrl(transferAnswer.getTransferUrl());
imageTransfer.setSignedTicketId(transferAnswer.getImageTransferId());
imageTransfer = imageTransferDao.persist(imageTransfer);
return imageTransfer;
}
@Override
public ImageTransferResponse createImageTransfer(CreateImageTransferCmd cmd) {
ImageTransfer imageTransfer;
if (cmd.getDirection().equals(ImageTransfer.Direction.upload)) {
imageTransfer = createUploadImageTransfer(cmd);
} else if (cmd.getDirection().equals(ImageTransfer.Direction.download)) {
imageTransfer = createDownloadImageTransfer(cmd);
} else {
throw new CloudRuntimeException("Invalid direction: " + cmd.getDirection());
}
ImageTransferVO imageTransferVO = imageTransferDao.findById(imageTransfer.getId());
ImageTransferResponse response = toImageTransferResponse(imageTransferVO);
return response;
}
private void finalizeDownloadImageTransfer(ImageTransferVO imageTransfer) {
String transferId = imageTransfer.getUuid();
int nbdPort = imageTransfer.getNbdPort();
String direction = imageTransfer.getDirection().toString();
FinalizeImageTransferCommand finalizeCmd = new FinalizeImageTransferCommand(transferId, direction, nbdPort);
BackupVO backup = backupDao.findById(imageTransfer.getBackupId());
boolean dummyOffering = isDummyOffering(backup.getBackupOfferingId());
FinalizeImageTransferCommand finalizeCmd = new FinalizeImageTransferCommand(imageTransfer.getUuid());
try {
Answer answer;
if (dummyOffering) {
@ -384,17 +450,56 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
}
if (!answer.getResult()) {
throw new CloudRuntimeException("Failed to create image transfer: " + answer.getDetails());
throw new CloudRuntimeException("Failed to finalize image transfer: " + answer.getDetails());
}
imageTransfer.setPhase(ImageTransferVO.Phase.finished);
imageTransferDao.update(imageTransferId, imageTransfer);
imageTransferDao.remove(imageTransferId);
} catch (AgentUnavailableException | OperationTimedoutException e) {
throw new CloudRuntimeException("Failed to communicate with agent: " + e.getMessage(), e);
}
}
private void finalizeUploadImageTransfer(ImageTransferVO imageTransfer) {
String transferId = imageTransfer.getUuid();
int nbdPort = imageTransfer.getNbdPort();
String direction = imageTransfer.getDirection().toString();
StopNBDServerCommand stopNbdServerCommand = new StopNBDServerCommand(transferId, direction, nbdPort);
Answer answer;
try {
answer = agentManager.send(imageTransfer.getHostId(), stopNbdServerCommand);
} catch (AgentUnavailableException | OperationTimedoutException e) {
throw new CloudRuntimeException("Failed to communicate with agent: " + e.getMessage(), e);
}
if (!answer.getResult()) {
throw new CloudRuntimeException("Failed to stop the nbd server");
}
FinalizeImageTransferCommand finalizeCmd = new FinalizeImageTransferCommand(transferId, direction, nbdPort);
EndPoint ssvm = _epSelector.findSsvm(imageTransfer.getDataCenterId());
answer = ssvm.sendMessage(finalizeCmd);
if (!answer.getResult()) {
throw new CloudRuntimeException("Failed to finalize image transfer: " + answer.getDetails());
}
}
@Override
public boolean finalizeImageTransfer(FinalizeImageTransferCmd cmd) {
Long imageTransferId = cmd.getImageTransferId();
ImageTransferVO imageTransfer = imageTransferDao.findById(imageTransferId);
if (imageTransfer == null) {
throw new CloudRuntimeException("Image transfer not found: " + imageTransferId);
}
if (imageTransfer.getDirection().equals(ImageTransfer.Direction.download)) {
finalizeDownloadImageTransfer(imageTransfer);
} else {
finalizeUploadImageTransfer(imageTransfer);
}
imageTransfer.setPhase(ImageTransferVO.Phase.finished);
imageTransferDao.update(imageTransfer.getId(), imageTransfer);
imageTransferDao.remove(imageTransfer.getId());
return true;
}
@ -463,43 +568,34 @@ public class IncrementalBackupServiceImpl extends ManagerBase implements Increme
return cmdList;
}
// Helper methods
private int allocateNbdPort() {
// Simplified port allocation for POC
private int getRandomNbdPort() {
Random random = new Random();
return NBD_PORT_RANGE_START + random.nextInt(NBD_PORT_RANGE_END - NBD_PORT_RANGE_START);
}
private String resolveDeviceName(List<? extends Volume> volumes, Long targetDiskId) {
// Simplified device name resolution for POC
int index = 0;
for (Volume vol : volumes) {
if (Long.valueOf(vol.getId()).equals(targetDiskId)) {
return "vd" + (char)('a' + index);
}
index++;
private int allocateNbdPort() {
int port = getRandomNbdPort();
while (imageTransferDao.findByNbdPort(port) != null) {
port = getRandomNbdPort();
}
return "vda"; // fallback
return port;
}
private ImageTransferResponse toImageTransferResponse(ImageTransferVO imageTransfer) {
private ImageTransferResponse toImageTransferResponse(ImageTransferVO imageTransferVO) {
ImageTransferResponse response = new ImageTransferResponse();
response.setId(imageTransfer.getUuid());
BackupVO backup = backupDao.findById(imageTransfer.getBackupId());
VMInstanceVO vm = vmInstanceDao.findById(imageTransfer.getVmId());
Volume volume = volumeDao.findById(imageTransfer.getDiskId());
if (backup != null) response.setBackupId(backup.getUuid());
if (vm != null) response.setVmId(vm.getUuid());
if (volume != null) response.setDiskId(volume.getUuid());
response.setDeviceName(imageTransfer.getDeviceName());
response.setTransferUrl(imageTransfer.getTransferUrl());
response.setPhase(imageTransfer.getPhase().toString());
response.setCreated(imageTransfer.getCreated());
response.setId(imageTransferVO.getUuid());
Long backupId = imageTransferVO.getBackupId();
if (backupId != null) {
Backup backup = backupDao.findById(backupId);
response.setBackupId(backup.getUuid());
}
Long volumeId = imageTransferVO.getDiskId();
Volume volume = volumeDao.findById(volumeId);
response.setDiskId(volume.getUuid());
response.setTransferUrl(imageTransferVO.getTransferUrl());
response.setPhase(ImageTransferVO.Phase.initializing.toString());
response.setDirection(imageTransferVO.getDirection().toString());
response.setCreated(imageTransferVO.getCreated());
return response;
}
}

View File

@ -3716,6 +3716,93 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S
return new QuerySnapshotZoneCopyAnswer(cmd, files);
}
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 boolean stopImageServer() {
String unitName = "cloudstack-image-server";
Script checkScript = new Script("/bin/bash", logger);
checkScript.add("-c");
checkScript.add(String.format("systemctl is-active --quiet %s", unitName));
String checkResult = checkScript.execute();
if (checkResult != null) {
logger.info(String.format("Image server not running, resetting failed state", unitName));
resetService(unitName);
return true;
}
Script stopScript = new Script("/bin/bash", logger);
stopScript.add("-c");
stopScript.add(String.format("systemctl stop %s", unitName));
stopScript.execute();
resetService(unitName);
logger.info(String.format("Image server %s stoppped", unitName));
return true;
}
private boolean startImageServerIfNotRunning(int imageServerPort) {
final String imageServerScript = "/opt/cloud/bin/image_server.py";
String unitName = "cloudstack-image-server";
Script checkScript = new Script("/bin/bash", logger);
checkScript.add("-c");
checkScript.add(String.format("systemctl is-active --quiet %s", unitName));
String checkResult = checkScript.execute();
if (checkResult == null) {
return true;
}
String systemdRunCmd = String.format(
"systemd-run --unit=%s --property=Restart=no /usr/bin/python3 %s --listen 0.0.0.0 --port 54323",
unitName, imageServerScript, imageServerPort);
Script startScript = new Script("/bin/bash", logger);
startScript.add("-c");
startScript.add(systemdRunCmd);
String startResult = startScript.execute();
if (startResult != null) {
logger.error(String.format("Failed to start the Image serer: %s", startResult));
return false;
}
// Wait with timeout until the service is up
int maxWaitSeconds = 10;
int pollIntervalMs = 1000;
int maxAttempts = (maxWaitSeconds * 1000) / pollIntervalMs;
boolean serviceActive = false;
for (int attempt = 0; attempt < maxAttempts; attempt++) {
Script verifyScript = new Script("/bin/bash", logger);
verifyScript.add("-c");
verifyScript.add(String.format("systemctl is-active --quiet %s", unitName));
String verifyResult = verifyScript.execute();
if (verifyResult == null) {
serviceActive = true;
logger.info(String.format("Image server is now active (attempt %d)", unitName, attempt + 1));
break;
}
try {
Thread.sleep(pollIntervalMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
if (!serviceActive) {
logger.error(String.format("Image server failed to start within %d seconds", unitName, maxWaitSeconds));
return false;
}
return true;
}
protected Answer execute(CreateImageTransferCommand cmd) {
if (!_inSystemVM) {
return new CreateImageTransferAnswer(cmd, true, "Not running inside SSVM; skipping image transfer setup.");
@ -3724,7 +3811,7 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S
final String transferId = cmd.getTransferId();
final String hostIp = cmd.getHostIpAddress();
final String exportName = cmd.getDeviceName();
final String exportName = cmd.getExportName();
final int nbdPort = cmd.getNbdPort();
if (StringUtils.isBlank(transferId)) {
@ -3734,15 +3821,13 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S
return new CreateImageTransferAnswer(cmd, false, "hostIpAddress is empty.");
}
if (StringUtils.isBlank(exportName)) {
return new CreateImageTransferAnswer(cmd, false, "deviceName is empty.");
return new CreateImageTransferAnswer(cmd, false, "exportName is empty.");
}
if (nbdPort <= 0) {
return new CreateImageTransferAnswer(cmd, false, "Invalid nbdPort: " + nbdPort);
}
final String imageServerScript = "/opt/cloud/bin/image_server.py";
final int imageServerPort = 54323;
final String imageServerLogFile = "/var/log/image_server.log";
try {
// 1) Write /tmp/<transferId> with NBD endpoint details.
@ -3752,40 +3837,22 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S
payload.put("export", exportName);
final String json = new GsonBuilder().create().toJson(payload);
final File transferFile = new File("/tmp", transferId);
File dir = new File("/tmp/imagetransfer");
if (!dir.exists()) {
dir.mkdirs();
}
final File transferFile = new File("/tmp/imagetransfer", transferId);
FileUtils.writeStringToFile(transferFile, json, "UTF-8");
// 2) Start image_server if not already running.
final File scriptFile = new File(imageServerScript);
if (!scriptFile.exists()) {
return new CreateImageTransferAnswer(cmd, false, "Missing image server script: " + imageServerScript);
}
final Script isRunning = new Script("/bin/bash", logger);
isRunning.add("-c");
isRunning.add(String.format("pgrep -f '%s.*--port %d' >/dev/null 2>&1", imageServerScript, imageServerPort));
final String runningResult = isRunning.execute();
if (runningResult != null) {
try {
ProcessBuilder pb = new ProcessBuilder(
"python3", imageServerScript,
"--listen", "0.0.0.0",
"--port", String.valueOf(imageServerPort)
);
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(new File(imageServerLogFile)));
pb.redirectErrorStream(true);
pb.start();
} catch (IOException e) {
logger.warn("Failed to start Image Server");
return new CreateImageTransferAnswer(cmd, false, "Failed to start image server");
}
}
final String transferUrl = String.format("http://%s:%d/images/%s", _publicIp, imageServerPort, transferId);
return new CreateImageTransferAnswer(cmd, true, "Image transfer prepared on SSVM.", transferId, transferUrl, "initializing");
} catch (Exception e) {
} catch (IOException e) {
logger.warn("Failed to prepare image transfer on SSVM", e);
return new CreateImageTransferAnswer(cmd, false, "Failed to prepare image transfer on SSVM: " + e.getMessage());
}
startImageServerIfNotRunning(imageServerPort);
final String transferUrl = String.format("http://%s:%d/images/%s", _publicIp, imageServerPort, transferId);
return new CreateImageTransferAnswer(cmd, true, "Image transfer prepared on SSVM.", transferId, transferUrl);
}
protected Answer execute(FinalizeImageTransferCommand cmd) {
@ -3798,26 +3865,17 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S
return new Answer(cmd, false, "transferId is empty.");
}
final File transferFile = new File("/tmp", transferId);
final File transferFile = new File("/tmp/imagetransfer", transferId);
if (transferFile.exists() && !transferFile.delete()) {
return new Answer(cmd, false, "Failed to delete transfer config file: " + transferFile.getAbsolutePath());
}
// Stop image_server.py only if /tmp directory is empty.
final File tmpDir = new File("/tmp");
final File[] tmpEntries = tmpDir.listFiles();
if (tmpEntries != null && tmpEntries.length == 0) {
final String imageServerScript = "/opt/cloud/bin/image_server.py";
final int imageServerPort = 54323;
// Use bash "|| true" so Script returns success even if process isn't running.
final Script stop = new Script("/bin/bash", logger);
stop.add("-c");
stop.add(String.format("pkill -f '%s.*--port %d' >/dev/null 2>&1 || true", imageServerScript, imageServerPort));
final String stopResult = stop.execute();
if (stopResult != null) {
return new Answer(cmd, false, "Failed to stop image server: " + stopResult);
try (Stream<Path> stream = Files.list(Paths.get("/tmp/imagetransfer"))) {
if (!stream.findAny().isPresent()) {
stopImageServer();
}
} catch (IOException e) {
logger.warn("Failed to list /tmp/imagetransfer", e);
}
return new Answer(cmd, true, "Image transfer finalized.");

View File

@ -62,11 +62,11 @@ _IMAGE_LOCKS_GUARD = threading.Lock()
# Dynamic image_id(transferId) -> NBD export mapping:
# CloudStack writes a JSON file at /tmp/<transferId> with:
# CloudStack writes a JSON file at /tmp/imagetransfer/<transferId> with:
# {"host": "...", "port": 10809, "export": "vda"}
#
# This server reads that file on-demand.
_CFG_DIR = "/tmp"
_CFG_DIR = "/tmp/imagetransfer"
_CFG_CACHE: Dict[str, Tuple[float, Dict[str, Any]]] = {}
_CFG_CACHE_GUARD = threading.Lock()