diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java index dab2e7459ca..b67128e47dc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java @@ -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 diff --git a/api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java b/api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java index 4a0cd04ea10..f43be2bdafe 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java +++ b/api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java @@ -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(); diff --git a/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java index 74dc261893c..34cf6d4ca34 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java +++ b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java @@ -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; - } } diff --git a/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java index f9dfd256c39..f826c01f3a6 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java @@ -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; + } } diff --git a/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java b/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java index 84d9b1ff818..f1a0285ef6e 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java @@ -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; diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java index 056cee41df7..7628fe19698 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java +++ b/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java @@ -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 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 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 getDeviceMappings() { - return deviceMappings; + public Boolean getIncremental() { + return isIncremental; } - public void setDeviceMappings(Map deviceMappings) { - this.deviceMappings = deviceMappings; + public void setIncremental(Boolean incremental) { + isIncremental = incremental; } } diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java index ac2cc8af70a..ba4daddc116 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java @@ -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 diskVolumePaths; // volumeId -> path mapping + private Map diskVolumePaths; // volumeId -> path mapping private String hostIpAddress; public StartBackupCommand() { } - public StartBackupCommand(String vmName, Long vmId, String toCheckpointId, String fromCheckpointId, - int nbdPort, Map diskVolumePaths, String hostIpAddress) { + public StartBackupCommand(String vmName, String toCheckpointId, String fromCheckpointId, + int nbdPort, Map 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 getDiskVolumePaths() { + public Map getDiskVolumePaths() { return diskVolumePaths; } diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerAnswer.java new file mode 100644 index 00000000000..d8c78d3c880 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerAnswer.java @@ -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; + } + +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerCommand.java new file mode 100644 index 00000000000..887937ffb4c --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerCommand.java @@ -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; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StopNBDServerCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StopNBDServerCommand.java new file mode 100644 index 00000000000..4f2b6401480 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StopNBDServerCommand.java @@ -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; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java index 4efad8d3fd1..d9bd1f4c9ba 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java @@ -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; } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java index e76be261cd8..e5e57c4acda 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java @@ -25,6 +25,6 @@ import com.cloud.utils.db.GenericDao; public interface ImageTransferDao extends GenericDao { List listByBackupId(Long backupId); - List listByVmId(Long vmId); ImageTransferVO findByUuid(String uuid); + ImageTransferVO findByNbdPort(int port); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java index 4c426d870ff..57587858661 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java @@ -32,8 +32,8 @@ import com.cloud.utils.db.SearchCriteria; public class ImageTransferDaoImpl extends GenericDaoBase implements ImageTransferDao { private SearchBuilder backupIdSearch; - private SearchBuilder vmIdSearch; private SearchBuilder uuidSearch; + private SearchBuilder nbdPortSearch; public ImageTransferDaoImpl() { } @@ -44,13 +44,13 @@ public class ImageTransferDaoImpl extends GenericDaoBase 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 return listBy(sc); } - @Override - public List listByVmId(Long vmId) { - SearchCriteria sc = vmIdSearch.create(); - sc.setParameters("vmId", vmId); - return listBy(sc); - } - @Override public ImageTransferVO findByUuid(String uuid) { SearchCriteria sc = uuidSearch.create(); sc.setParameters("uuid", uuid); return findOneBy(sc); } + + @Override + public ImageTransferVO findByNbdPort(int port) { + SearchCriteria sc = nbdPortSearch.create(); + sc.setParameters("nbdPort", port); + return findOneBy(sc); + } } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index e0b0ec48a02..d3ee808cbac 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -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; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java index 1c3ec2ae3dc..1db594d169f 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java @@ -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 { 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); + } + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java index 57fb39473a2..5013e4d7972 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java @@ -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 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\n"); // Add disk entries - simplified for POC - Map diskPaths = cmd.getDiskVolumePaths(); + Map diskPaths = cmd.getDiskVolumePaths(); int diskIndex = 0; - for (Map.Entry entry : diskPaths.entrySet()) { + for (Map.Entry entry : diskPaths.entrySet()) { String deviceName = "vd" + (char)('a' + diskIndex); String scratchFile = "/var/tmp/scratch-" + entry.getKey() + ".qcow2"; xml.append(" \n"); + .append(entry.getKey()).append("\">\n"); xml.append(" \n"); xml.append(" \n"); diskIndex++; @@ -141,19 +136,4 @@ public class LibvirtStartBackupCommandWrapper extends CommandWrapper" + checkpointId + "\n" + ""; } - - private Map getDeviceMappings(String vmName, Map diskPaths, - LibvirtComputingResource resource) { - Map 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; - } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartNBDServerCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartNBDServerCommandWrapper.java new file mode 100644 index 00000000000..c7f2e8d6d08 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartNBDServerCommandWrapper.java @@ -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 { + 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); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java new file mode 100644 index 00000000000..96ac0e7accc --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java @@ -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 { + 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); + } + + } +} diff --git a/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java b/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java index 0723b49bd2e..2eace1ff1ba 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/IncrementalBackupServiceImpl.java @@ -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 volumes = volumeDao.findByInstance(vmId); - Map diskVolumePaths = new HashMap<>(); + List volumes = volumeDao.findByInstance(vmId); + Map 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 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 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 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; } } diff --git a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java index 0f8de122d88..8b3df590159 100644 --- a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java +++ b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java @@ -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/ 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 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."); diff --git a/systemvm/debian/opt/cloud/bin/image_server.py b/systemvm/debian/opt/cloud/bin/image_server.py index 2a9013fb4c5..28513371e9d 100644 --- a/systemvm/debian/opt/cloud/bin/image_server.py +++ b/systemvm/debian/opt/cloud/bin/image_server.py @@ -62,11 +62,11 @@ _IMAGE_LOCKS_GUARD = threading.Lock() # Dynamic image_id(transferId) -> NBD export mapping: -# CloudStack writes a JSON file at /tmp/ with: +# CloudStack writes a JSON file at /tmp/imagetransfer/ 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()