Skip to content

Commit a1959f2

Browse files
shwstpprabh1sarweizhouapache
authored
backup: veeam kvm integration (#12991)
This PR introduces the initial implementation of Veeam integration support for KVM in CloudStack by adding a UHAPI-compatible server and image server components. Veeam Backup & Replication interacts with virtualization platforms using its Universal Hypervisor API (UHAPI). To enable backup and restore workflows for CloudStack-managed KVM environments, this change introduces a UHAPI server that exposes CloudStack resources through a UHAPI-compatible interface. In addition to the control plane APIs, an image server component is introduced to handle the data transfer operations required during backup and restore workflows. The integration consists of two main components: 1. UHAPI Server (Control Plane) named CloudStack Veeam Control Service A lightweight UHAPI server runs inside the CloudStack management server and exposes endpoints under: /ovirt-engine - /api - For APIs - /sso - For authentication - /services/pki-resource - For certificates This server provides inventory discovery APIs required by Veeam and translates CloudStack resources into the structures expected by UHAPI. The server: - exposes infrastructure inventory - handles authentication and session tokens - maps CloudStack resources to UHAPI-compatible representations 2. Image Server (Data Plane) named CloudStack Image Service A separate image server component is introduced to handle backup and restore data transfer operations. This component: - serves disk image data during backup - receives image data during restore operations - exposes endpoints used by Veeam worker components - integrates with CloudStack storage to read and write VM disk data The separation between both these components server ensures that: - metadata APIs and control operations remain lightweight - bulk image transfer operations are handled independently Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> Co-authored-by: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Co-authored-by: abh1sar <abhisar.sinha@gmail.com> Co-authored-by: Wei Zhou <weizhou@apache.org>
1 parent be51948 commit a1959f2

329 files changed

Lines changed: 34111 additions & 352 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agent/conf/agent.properties

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ zone=default
7878
# Generated with "uuidgen".
7979
local.storage.uuid=
8080

81+
# Enable TLS for image server transfers. The keys are read from:
82+
# cert file = /etc/cloudstack/agent/cloud.crt
83+
# key file = /etc/cloudstack/agent/cloud.key
84+
image.server.tls.enabled=true
85+
86+
# The Address for the network interface that the image server listens on. If not specified, it will listen on the Management network.
87+
#image.server.listen.address=
88+
8189
# Location for KVM virtual router scripts.
8290
# The path defined in this property is relative to the directory "/usr/share/cloudstack-common/".
8391
domr.scripts.dir=scripts/network/domr/kvm

agent/src/main/java/com/cloud/agent/properties/AgentProperties.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,20 @@ public class AgentProperties{
123123
*/
124124
public static final Property<String> LOCAL_STORAGE_PATH = new Property<>("local.storage.path", "/var/lib/libvirt/images/");
125125

126+
/**
127+
* Enables TLS on the KVM image server transfer endpoint.<br>
128+
* Data type: Boolean.<br>
129+
* Default value: <code>true</code>
130+
*/
131+
public static final Property<Boolean> IMAGE_SERVER_TLS_ENABLED = new Property<>("image.server.tls.enabled", true);
132+
133+
/**
134+
* The IP address that the KVM image server listens on.<br>
135+
* Data type: String.<br>
136+
* Default value: <code>null</code>
137+
*/
138+
public static final Property<String> IMAGE_SERVER_LISTEN_ADDRESS = new Property<>("image.server.listen.address", null, String.class);
139+
126140
/**
127141
* Directory where Qemu sockets are placed.<br>
128142
* These sockets are for the Qemu Guest Agent and SSVM provisioning.<br>

api/src/main/java/com/cloud/storage/VolumeApiService.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Map;
2424

25+
import com.cloud.dc.DataCenter;
2526
import com.cloud.exception.ResourceAllocationException;
2627
import com.cloud.offering.DiskOffering;
2728
import com.cloud.user.Account;
@@ -70,6 +71,10 @@ public interface VolumeApiService {
7071
*/
7172
Volume allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationException;
7273

74+
Volume allocVolume(long ownerId, Long zoneId, Long diskOfferingId, Long vmId, Long snapshotId, String name,
75+
Long cmdSize, Boolean displayVolume, Long cmdMinIops, Long cmdMaxIops, String customId)
76+
throws ResourceAllocationException;
77+
7378
/**
7479
* Creates the volume based on the given criteria
7580
*
@@ -80,6 +85,8 @@ public interface VolumeApiService {
8085
*/
8186
Volume createVolume(CreateVolumeCmd cmd);
8287

88+
Volume createVolume(long volumeId, Long vmId, Long snapshotId, Long storageId, Boolean display);
89+
8390
/**
8491
* Resizes the volume based on the given criteria
8592
*
@@ -203,4 +210,6 @@ Volume updateVolume(long volumeId, String path, String state, Long storageId,
203210
Pair<String, String> checkAndRepairVolume(CheckAndRepairVolumeCmd cmd) throws ResourceAllocationException;
204211

205212
Long getVolumePhysicalSize(Storage.ImageFormat format, String path, String chainInfo);
213+
214+
Long getCustomDiskOfferingIdForVolumeUpload(Account owner, DataCenter zone, boolean encryptEnabledOnly);
206215
}

api/src/main/java/com/cloud/user/AccountService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,16 @@ User createUser(String userName, String password, String firstName, String lastN
8888

8989
Account getActiveAccountById(long accountId);
9090

91+
Account getActiveAccountByUuid(String accountUuid);
92+
9193
Account getAccount(long accountId);
9294

95+
Account getAccountByUuid(String accountUuid);
96+
9397
User getActiveUser(long userId);
9498

99+
User getOneActiveUserForAccount(Account account);
100+
95101
User getUserIncludingRemoved(long userId);
96102

97103
boolean isRootAdmin(Long accountId);

api/src/main/java/com/cloud/vm/VmDetailConstants.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,10 @@ public interface VmDetailConstants {
130130
String EXTERNAL_DETAIL_PREFIX = "External:";
131131
String CLOUDSTACK_VM_DETAILS = "cloudstack.vm.details";
132132
String CLOUDSTACK_VLAN = "cloudstack.vlan";
133+
134+
// KVM Checkpoints related
135+
String ACTIVE_CHECKPOINT_ID = "active.checkpoint.id";
136+
String ACTIVE_CHECKPOINT_CREATE_TIME = "active.checkpoint.create.time";
137+
String LAST_CHECKPOINT_ID = "last.checkpoint.id";
138+
String LAST_CHECKPOINT_CREATE_TIME = "last.checkpoint.create.time";
133139
}

api/src/main/java/org/apache/cloudstack/api/ApiConstants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public class ApiConstants {
7979
public static final String BOOTABLE = "bootable";
8080
public static final String BIND_DN = "binddn";
8181
public static final String BIND_PASSWORD = "bindpass";
82+
public static final String BLANK_INSTANCE = "blankinstance";
8283
public static final String BUS_ADDRESS = "busaddress";
8384
public static final String BYTES_READ_RATE = "bytesreadrate";
8485
public static final String BYTES_READ_RATE_MAX = "bytesreadratemax";
@@ -220,6 +221,7 @@ public class ApiConstants {
220221
public static final String DOMAIN_PATH = "domainpath";
221222
public static final String DOMAIN_ID = "domainid";
222223
public static final String DOMAIN__ID = "domainId";
224+
public static final String DUMMY = "dummy";
223225
public static final String DURATION = "duration";
224226
public static final String ELIGIBLE = "eligible";
225227
public static final String EMAIL = "email";
@@ -263,6 +265,7 @@ public class ApiConstants {
263265
public static final String FOR_VIRTUAL_NETWORK = "forvirtualnetwork";
264266
public static final String FOR_SYSTEM_VMS = "forsystemvms";
265267
public static final String FOR_PROVIDER = "forprovider";
268+
public static final String FROM_CHECKPOINT_ID = "fromcheckpointid";
266269
public static final String FULL_PATH = "fullpath";
267270
public static final String GATEWAY = "gateway";
268271
public static final String IP6_GATEWAY = "ip6gateway";
@@ -335,6 +338,7 @@ public class ApiConstants {
335338
public static final String IS_2FA_VERIFIED = "is2faverified";
336339

337340
public static final String IS_2FA_MANDATED = "is2famandated";
341+
public static final String IS_ACTIVE = "isactive";
338342
public static final String IS_ASYNC = "isasync";
339343
public static final String IP_AVAILABLE = "ipavailable";
340344
public static final String IP_LIMIT = "iplimit";
@@ -611,6 +615,7 @@ public class ApiConstants {
611615
public static final String TENANT_NAME = "tenantname";
612616
public static final String TOTAL = "total";
613617
public static final String TOTAL_SUBNETS = "totalsubnets";
618+
public static final String TO_CHECKPOINT_ID = "tocheckpointid";
614619
public static final String TOTAL_QUOTA = "totalquota";
615620
public static final String TYPE = "type";
616621
public static final String TRUST_STORE = "truststore";

api/src/main/java/org/apache/cloudstack/api/ApiServerService.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121

2222
import javax.servlet.http.HttpSession;
2323

24+
import org.apache.cloudstack.context.CallContext;
25+
2426
import com.cloud.domain.Domain;
2527
import com.cloud.exception.CloudAuthenticationException;
28+
import com.cloud.user.Account;
2629
import com.cloud.user.UserAccount;
2730

2831
public interface ApiServerService {
@@ -52,4 +55,20 @@ public ResponseObject loginUser(HttpSession session, String username, String pas
5255
String getDomainId(Map<String, Object[]> params);
5356

5457
boolean isPostRequestsAndTimestampsEnforced();
58+
59+
AsyncCmdResult processAsyncCmd(BaseAsyncCmd cmdObj, Map<String, String> params, CallContext ctx, Long callerUserId, Account caller) throws Exception;
60+
61+
class AsyncCmdResult {
62+
public final Long objectId;
63+
public final String objectUuid;
64+
public final BaseAsyncCmd asyncCmd;
65+
public final long jobId;
66+
67+
public AsyncCmdResult(Long objectId, String objectUuid, BaseAsyncCmd asyncCmd, long jobId) {
68+
this.objectId = objectId;
69+
this.objectUuid = objectUuid;
70+
this.asyncCmd = asyncCmd;
71+
this.jobId = jobId;
72+
}
73+
}
5574
}

api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public abstract class BaseAsyncCreateCustomIdCmd extends BaseAsyncCreateCmd {
2020
@Parameter(name = ApiConstants.CUSTOM_ID,
2121
type = CommandType.STRING,
2222
description = "An optional field, in case you want to set a custom id to the resource. Allowed to Root Admins only")
23-
private String customId;
23+
protected String customId;
2424

2525
public String getCustomId() {
2626
return customId;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//Licensed to the Apache Software Foundation (ASF) under one
2+
//or more contributor license agreements. See the NOTICE file
3+
//distributed with this work for additional information
4+
//regarding copyright ownership. The ASF licenses this file
5+
//to you under the Apache License, Version 2.0 (the
6+
//"License"); you may not use this file except in compliance
7+
//the License. You may obtain a copy of the License at
8+
//
9+
//http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
//Unless required by applicable law or agreed to in writing,
12+
//software distributed under the License is distributed on an
13+
//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
//KIND, either express or implied. See the License for the
15+
//specific language governing permissions and limitations
16+
//under the License.
17+
18+
package org.apache.cloudstack.api.command.admin.backup;
19+
20+
import javax.inject.Inject;
21+
22+
import org.apache.cloudstack.acl.RoleType;
23+
import org.apache.cloudstack.api.APICommand;
24+
import org.apache.cloudstack.api.ApiConstants;
25+
import org.apache.cloudstack.api.BaseCmd;
26+
import org.apache.cloudstack.api.Parameter;
27+
import org.apache.cloudstack.api.command.admin.AdminCmd;
28+
import org.apache.cloudstack.api.response.BackupResponse;
29+
import org.apache.cloudstack.api.response.ImageTransferResponse;
30+
import org.apache.cloudstack.api.response.VolumeResponse;
31+
import org.apache.cloudstack.backup.ImageTransfer;
32+
import org.apache.cloudstack.backup.KVMBackupExportService;
33+
import org.apache.cloudstack.context.CallContext;
34+
35+
import com.cloud.utils.EnumUtils;
36+
37+
@APICommand(name = "createImageTransfer",
38+
description = "Create image transfer for a disk in backup. This API is intended for testing only and is disabled by default.",
39+
responseObject = ImageTransferResponse.class,
40+
since = "4.23.0",
41+
authorized = {RoleType.Admin})
42+
public class CreateImageTransferCmd extends BaseCmd implements AdminCmd {
43+
44+
@Inject
45+
private KVMBackupExportService kvmBackupExportService;
46+
47+
@Parameter(name = ApiConstants.BACKUP_ID,
48+
type = CommandType.UUID,
49+
entityType = BackupResponse.class,
50+
description = "ID of the backup")
51+
private Long backupId;
52+
53+
@Parameter(name = ApiConstants.VOLUME_ID,
54+
type = CommandType.UUID,
55+
entityType = VolumeResponse.class,
56+
required = true,
57+
description = "ID of the disk/volume")
58+
private Long volumeId;
59+
60+
@Parameter(name = ApiConstants.DIRECTION,
61+
type = CommandType.STRING,
62+
required = true,
63+
description = "Direction of the transfer: upload, download")
64+
private String direction;
65+
66+
@Parameter(name = ApiConstants.FORMAT,
67+
type = CommandType.STRING,
68+
description = "Format for the image transfer: raw/cow. 'raw' will create an NBD backend. 'cow' will use the File backend." +
69+
"For download, only the 'raw' format is supported. Default: raw")
70+
private String format;
71+
72+
public Long getBackupId() {
73+
return backupId;
74+
}
75+
76+
public Long getVolumeId() {
77+
return volumeId;
78+
}
79+
80+
public ImageTransfer.Direction getDirection() {
81+
return ImageTransfer.Direction.valueOf(direction);
82+
}
83+
84+
public ImageTransfer.Format getFormat() {
85+
return EnumUtils.getEnum(ImageTransfer.Format.class, format);
86+
}
87+
88+
@Override
89+
public void execute() {
90+
ImageTransferResponse response = kvmBackupExportService.createImageTransfer(this);
91+
response.setObjectName(ImageTransfer.class.getSimpleName().toLowerCase());
92+
response.setResponseName(getCommandName());
93+
setResponseObject(response);
94+
}
95+
96+
@Override
97+
public long getEntityOwnerId() {
98+
return CallContext.current().getCallingAccount().getId();
99+
}
100+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//Licensed to the Apache Software Foundation (ASF) under one
2+
//or more contributor license agreements. See the NOTICE file
3+
//distributed with this work for additional information
4+
//regarding copyright ownership. The ASF licenses this file
5+
//to you under the Apache License, Version 2.0 (the
6+
//"License"); you may not use this file except in compliance
7+
//the License. You may obtain a copy of the License at
8+
//
9+
//http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
//Unless required by applicable law or agreed to in writing,
12+
//software distributed under the License is distributed on an
13+
//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
//KIND, either express or implied. See the License for the
15+
//specific language governing permissions and limitations
16+
//under the License.
17+
18+
package org.apache.cloudstack.api.command.admin.backup;
19+
20+
import javax.inject.Inject;
21+
22+
import org.apache.cloudstack.acl.RoleType;
23+
import org.apache.cloudstack.api.APICommand;
24+
import org.apache.cloudstack.api.ApiConstants;
25+
import org.apache.cloudstack.api.BaseCmd;
26+
import org.apache.cloudstack.api.Parameter;
27+
import org.apache.cloudstack.api.command.admin.AdminCmd;
28+
import org.apache.cloudstack.api.response.SuccessResponse;
29+
import org.apache.cloudstack.api.response.UserVmResponse;
30+
import org.apache.cloudstack.backup.KVMBackupExportService;
31+
import org.apache.cloudstack.context.CallContext;
32+
33+
@APICommand(name = "deleteVirtualMachineCheckpoint",
34+
description = "Delete a VM checkpoint. This API is intended for testing only and is disabled by default.",
35+
responseObject = SuccessResponse.class,
36+
since = "4.23.0",
37+
authorized = {RoleType.Admin})
38+
public class DeleteVmCheckpointCmd extends BaseCmd implements AdminCmd {
39+
40+
@Inject
41+
private KVMBackupExportService kvmBackupExportService;
42+
43+
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
44+
type = CommandType.UUID,
45+
entityType = UserVmResponse.class,
46+
required = true,
47+
description = "ID of the VM")
48+
private Long vmId;
49+
50+
@Parameter(name = "checkpointid",
51+
type = CommandType.STRING,
52+
required = true,
53+
description = "Checkpoint ID")
54+
private String checkpointId;
55+
56+
public Long getVmId() {
57+
return vmId;
58+
}
59+
60+
public String getCheckpointId() {
61+
return checkpointId;
62+
}
63+
64+
public void setVmId(Long vmId) {
65+
this.vmId = vmId;
66+
}
67+
68+
public void setCheckpointId(String checkpointId) {
69+
this.checkpointId = checkpointId;
70+
}
71+
72+
@Override
73+
public void execute() {
74+
boolean result = kvmBackupExportService.deleteVmCheckpoint(this);
75+
SuccessResponse response = new SuccessResponse(getCommandName());
76+
response.setSuccess(result);
77+
response.setResponseName(getCommandName());
78+
setResponseObject(response);
79+
}
80+
81+
@Override
82+
public long getEntityOwnerId() {
83+
return CallContext.current().getCallingAccount().getId();
84+
}
85+
}

0 commit comments

Comments
 (0)