Skip to content

Commit 543c54c

Browse files
authored
api,server,ui: snapshot copy, multi-zone replica (apache#7873)
This PR adds new functionality to copy snapshots across zones and take snapshots for multiple zones. Copy functionality is similar to template copy. The source zone acts as the web server from where the destination zone(s) can download the snapshot files. For this purpose, a new API - `copySnapshot` has been added. The response for copySnapshot will be returning zone and download details from the first destination zone of the request. This behaviour is similar to the `copyTemplate` API. In a similar manner, multiple zones can be selected while taking the snapshots or creating snapshot policies. For this snapshot will be taken in the base zone(in which volume is present) and then copied to the additional zones. A new parameter - `zoneids` has been added to `createSnapshot` and `createSnapshotPolicy` APIs. As snapshots can be present on multiple zones (secondary stores), a new parameter `zoneid` has been added to delete the snapshot copy on a specific zone. `listSnapshots` API has been updated to allow listing snapshot entries for different zones/datastores. New parameters - `showUnique`, `locationType` have been added. Events generated during snapshot operations will now be linked to the snapshot itself rather than the volume of the snapshot. `listSnapshotPolicies` and `createSnapshotPolicy` APIs will return zone details of the zones in which backup will be scheduled for the policy. ---- New API added `copySnapshot` Request and response params updated for APIs ``` - listSnapshots - deleteSnapshot - createTemplate - listZones - listSnapshotPolicies - createSnapshotPolicy ``` UI updated for - Snapshot detail view - Create snapshot form - Create snapshot policy form - Create volume (from snapshot) form - Create template (from snapshot) form Doc PR: apache/cloudstack-documentation#344 PR: apache#7873
1 parent 99ded81 commit 543c54c

132 files changed

Lines changed: 7231 additions & 1105 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.

api/src/main/java/com/cloud/event/EventTypes.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.cloudstack.config.Configuration;
3131
import org.apache.cloudstack.ha.HAConfig;
3232
import org.apache.cloudstack.usage.Usage;
33+
import org.apache.cloudstack.vm.schedule.VMSchedule;
3334

3435
import com.cloud.dc.DataCenter;
3536
import com.cloud.dc.DataCenterGuestIpv6Prefix;
@@ -84,7 +85,6 @@
8485
import com.cloud.vm.Nic;
8586
import com.cloud.vm.NicSecondaryIp;
8687
import com.cloud.vm.VirtualMachine;
87-
import org.apache.cloudstack.vm.schedule.VMSchedule;
8888

8989
public class EventTypes {
9090

@@ -320,6 +320,7 @@ public class EventTypes {
320320
public static final String EVENT_DOMAIN_UPDATE = "DOMAIN.UPDATE";
321321

322322
// Snapshots
323+
public static final String EVENT_SNAPSHOT_COPY = "SNAPSHOT.COPY";
323324
public static final String EVENT_SNAPSHOT_CREATE = "SNAPSHOT.CREATE";
324325
public static final String EVENT_SNAPSHOT_ON_PRIMARY = "SNAPSHOT.ON_PRIMARY";
325326
public static final String EVENT_SNAPSHOT_OFF_PRIMARY = "SNAPSHOT.OFF_PRIMARY";

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
package com.cloud.storage;
2020

2121
import java.net.MalformedURLException;
22+
import java.util.List;
2223
import java.util.Map;
2324

24-
import com.cloud.utils.fsm.NoTransitionException;
2525
import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd;
2626
import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd;
2727
import org.apache.cloudstack.api.command.user.volume.ChangeOfferingForVolumeCmd;
@@ -37,6 +37,7 @@
3737

3838
import com.cloud.exception.ResourceAllocationException;
3939
import com.cloud.user.Account;
40+
import com.cloud.utils.fsm.NoTransitionException;
4041

4142
public interface VolumeApiService {
4243

@@ -105,10 +106,10 @@ public interface VolumeApiService {
105106

106107
Volume detachVolumeFromVM(DetachVolumeCmd cmd);
107108

108-
Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, Map<String, String> tags)
109+
Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, Map<String, String> tags, List<Long> zoneIds)
109110
throws ResourceAllocationException;
110111

111-
Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType) throws ResourceAllocationException;
112+
Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, List<Long> zoneIds) throws ResourceAllocationException;
112113

113114
Volume updateVolume(long volumeId, String path, String state, Long storageId, Boolean displayVolume, String customId, long owner, String chainInfo, String name);
114115

api/src/main/java/com/cloud/storage/snapshot/SnapshotApiService.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,20 @@
1818

1919
import java.util.List;
2020

21+
import org.apache.cloudstack.api.command.user.snapshot.CopySnapshotCmd;
2122
import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotPolicyCmd;
2223
import org.apache.cloudstack.api.command.user.snapshot.DeleteSnapshotPoliciesCmd;
2324
import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotPoliciesCmd;
2425
import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotsCmd;
26+
import org.apache.cloudstack.api.command.user.snapshot.UpdateSnapshotPolicyCmd;
2527

2628
import com.cloud.api.commands.ListRecurringSnapshotScheduleCmd;
2729
import com.cloud.exception.ResourceAllocationException;
30+
import com.cloud.exception.StorageUnavailableException;
2831
import com.cloud.storage.Snapshot;
2932
import com.cloud.storage.Volume;
3033
import com.cloud.user.Account;
3134
import com.cloud.utils.Pair;
32-
import org.apache.cloudstack.api.command.user.snapshot.UpdateSnapshotPolicyCmd;
3335

3436
public interface SnapshotApiService {
3537

@@ -50,7 +52,7 @@ public interface SnapshotApiService {
5052
* @param snapshotId
5153
* TODO
5254
*/
53-
boolean deleteSnapshot(long snapshotId);
55+
boolean deleteSnapshot(long snapshotId, Long zoneId);
5456

5557
/**
5658
* Creates a policy with specified schedule. maxSnaps specifies the number of most recent snapshots that are to be
@@ -88,7 +90,7 @@ public interface SnapshotApiService {
8890

8991
Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType) throws ResourceAllocationException;
9092

91-
Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, Boolean isFromVmSnapshot)
93+
Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType, Boolean isFromVmSnapshot, List<Long> zoneIds)
9294
throws ResourceAllocationException;
9395

9496

@@ -124,4 +126,6 @@ Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapsh
124126
SnapshotPolicy updateSnapshotPolicy(UpdateSnapshotPolicyCmd updateSnapshotPolicyCmd);
125127

126128
void markVolumeSnapshotsAsDestroyed(Volume volume);
129+
130+
Snapshot copySnapshot(CopySnapshotCmd cmd) throws StorageUnavailableException, ResourceAllocationException;
127131
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ public class ApiConstants {
7676
public static final String CSR = "csr";
7777
public static final String PRIVATE_KEY = "privatekey";
7878
public static final String DATASTORE_HOST = "datastorehost";
79+
public static final String DATASTORE_ID = "datastoreid";
7980
public static final String DATASTORE_NAME = "datastorename";
8081
public static final String DATASTORE_PATH = "datastorepath";
82+
public static final String DATASTORE_STATE = "datastorestate";
8183
public static final String DATASTORE_TYPE = "datastoretype";
8284
public static final String DOMAIN_SUFFIX = "domainsuffix";
8385
public static final String DNS_SEARCH_ORDER = "dnssearchorder";
@@ -492,6 +494,7 @@ public class ApiConstants {
492494
public static final String ZONE = "zone";
493495
public static final String ZONE_ID = "zoneid";
494496
public static final String ZONE_NAME = "zonename";
497+
public static final String ZONE_WISE = "zonewise";
495498
public static final String NETWORK_TYPE = "networktype";
496499
public static final String PAGE = "page";
497500
public static final String PAGE_SIZE = "pagesize";
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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+
// with 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.user.snapshot;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import org.apache.cloudstack.acl.RoleType;
24+
import org.apache.cloudstack.api.APICommand;
25+
import org.apache.cloudstack.api.ApiCommandResourceType;
26+
import org.apache.cloudstack.api.ApiConstants;
27+
import org.apache.cloudstack.api.ApiErrorCode;
28+
import org.apache.cloudstack.api.BaseAsyncCmd;
29+
import org.apache.cloudstack.api.Parameter;
30+
import org.apache.cloudstack.api.ResponseObject;
31+
import org.apache.cloudstack.api.ServerApiException;
32+
import org.apache.cloudstack.api.command.user.UserCmd;
33+
import org.apache.cloudstack.api.response.SnapshotResponse;
34+
import org.apache.cloudstack.api.response.ZoneResponse;
35+
import org.apache.cloudstack.context.CallContext;
36+
import org.apache.commons.collections.CollectionUtils;
37+
import org.apache.log4j.Logger;
38+
39+
import com.cloud.dc.DataCenter;
40+
import com.cloud.event.EventTypes;
41+
import com.cloud.exception.ResourceAllocationException;
42+
import com.cloud.exception.ResourceUnavailableException;
43+
import com.cloud.exception.StorageUnavailableException;
44+
import com.cloud.storage.Snapshot;
45+
import com.cloud.user.Account;
46+
47+
@APICommand(name = "copySnapshot", description = "Copies a snapshot from one zone to another.",
48+
responseObject = SnapshotResponse.class, responseView = ResponseObject.ResponseView.Restricted,
49+
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0",
50+
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
51+
public class CopySnapshotCmd extends BaseAsyncCmd implements UserCmd {
52+
public static final Logger s_logger = Logger.getLogger(CopySnapshotCmd.class.getName());
53+
54+
/////////////////////////////////////////////////////
55+
//////////////// API parameters /////////////////////
56+
/////////////////////////////////////////////////////
57+
58+
@Parameter(name = ApiConstants.ID, type = CommandType.UUID,
59+
entityType = SnapshotResponse.class, required = true, description = "the ID of the snapshot.")
60+
private Long id;
61+
62+
@Parameter(name = ApiConstants.SOURCE_ZONE_ID,
63+
type = CommandType.UUID,
64+
entityType = ZoneResponse.class,
65+
description = "The ID of the zone in which the snapshot is currently present. " +
66+
"If not specified then the zone of snapshot's volume will be used.")
67+
private Long sourceZoneId;
68+
69+
@Parameter(name = ApiConstants.DESTINATION_ZONE_ID,
70+
type = CommandType.UUID,
71+
entityType = ZoneResponse.class,
72+
required = false,
73+
description = "The ID of the zone the snapshot is being copied to.")
74+
protected Long destZoneId;
75+
76+
@Parameter(name = ApiConstants.DESTINATION_ZONE_ID_LIST,
77+
type=CommandType.LIST,
78+
collectionType = CommandType.UUID,
79+
entityType = ZoneResponse.class,
80+
required = false,
81+
description = "A comma-separated list of IDs of the zones that the snapshot needs to be copied to. " +
82+
"Specify this list if the snapshot needs to copied to multiple zones in one go. " +
83+
"Do not specify destzoneid and destzoneids together, however one of them is required.")
84+
protected List<Long> destZoneIds;
85+
86+
/////////////////////////////////////////////////////
87+
/////////////////// Accessors ///////////////////////
88+
/////////////////////////////////////////////////////
89+
90+
91+
public Long getId() {
92+
return id;
93+
}
94+
95+
public Long getSourceZoneId() {
96+
return sourceZoneId;
97+
}
98+
99+
public List<Long> getDestinationZoneIds() {
100+
if (destZoneIds != null && destZoneIds.size() != 0) {
101+
return destZoneIds;
102+
}
103+
if (destZoneId != null) {
104+
List < Long > destIds = new ArrayList<>();
105+
destIds.add(destZoneId);
106+
return destIds;
107+
}
108+
return null;
109+
}
110+
111+
@Override
112+
public String getEventType() {
113+
return EventTypes.EVENT_SNAPSHOT_COPY;
114+
}
115+
116+
@Override
117+
public String getEventDescription() {
118+
StringBuilder descBuilder = new StringBuilder();
119+
if (getDestinationZoneIds() != null) {
120+
for (Long destId : getDestinationZoneIds()) {
121+
descBuilder.append(", ");
122+
descBuilder.append(_uuidMgr.getUuid(DataCenter.class, destId));
123+
}
124+
if (descBuilder.length() > 0) {
125+
descBuilder.deleteCharAt(0);
126+
}
127+
}
128+
129+
return "copying snapshot: " + _uuidMgr.getUuid(Snapshot.class, getId()) + ((descBuilder.length() > 0) ? " to zones: " + descBuilder.toString() : "");
130+
}
131+
132+
@Override
133+
public ApiCommandResourceType getApiResourceType() {
134+
return ApiCommandResourceType.Snapshot;
135+
}
136+
137+
@Override
138+
public Long getApiResourceId() {
139+
return getId();
140+
}
141+
142+
@Override
143+
public long getEntityOwnerId() {
144+
Snapshot snapshot = _entityMgr.findById(Snapshot.class, getId());
145+
if (snapshot != null) {
146+
return snapshot.getAccountId();
147+
}
148+
return Account.ACCOUNT_ID_SYSTEM;
149+
}
150+
151+
@Override
152+
public void execute() throws ResourceUnavailableException {
153+
try {
154+
if (destZoneId == null && CollectionUtils.isEmpty(destZoneIds))
155+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
156+
"Either destzoneid or destzoneids parameters have to be specified.");
157+
158+
if (destZoneId != null && CollectionUtils.isNotEmpty(destZoneIds))
159+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
160+
"Both destzoneid and destzoneids cannot be specified at the same time.");
161+
162+
CallContext.current().setEventDetails(getEventDescription());
163+
Snapshot snapshot = _snapshotService.copySnapshot(this);
164+
165+
if (snapshot != null) {
166+
SnapshotResponse response = _queryService.listSnapshot(this);
167+
response.setResponseName(getCommandName());
168+
setResponseObject(response);
169+
} else {
170+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to copy snapshot");
171+
}
172+
} catch (StorageUnavailableException ex) {
173+
s_logger.warn("Exception: ", ex);
174+
throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, ex.getMessage());
175+
} catch (ResourceAllocationException ex) {
176+
s_logger.warn("Exception: ", ex);
177+
throw new ServerApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR, ex.getMessage());
178+
}
179+
180+
}
181+
}

api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.Collection;
2020
import java.util.HashMap;
21+
import java.util.List;
2122
import java.util.Map;
2223

2324
import org.apache.cloudstack.api.APICommand;
@@ -32,6 +33,7 @@
3233
import org.apache.cloudstack.api.response.SnapshotPolicyResponse;
3334
import org.apache.cloudstack.api.response.SnapshotResponse;
3435
import org.apache.cloudstack.api.response.VolumeResponse;
36+
import org.apache.cloudstack.api.response.ZoneResponse;
3537
import org.apache.commons.collections.MapUtils;
3638
import org.apache.log4j.Logger;
3739

@@ -90,6 +92,15 @@ public class CreateSnapshotCmd extends BaseAsyncCreateCmd {
9092
@Parameter(name = ApiConstants.TAGS, type = CommandType.MAP, description = "Map of tags (key/value pairs)")
9193
private Map tags;
9294

95+
@Parameter(name = ApiConstants.ZONE_ID_LIST,
96+
type=CommandType.LIST,
97+
collectionType = CommandType.UUID,
98+
entityType = ZoneResponse.class,
99+
description = "A comma-separated list of IDs of the zones in which the snapshot will be made available. " +
100+
"The snapshot will always be made available in the zone in which the volume is present.",
101+
since = "4.19.0")
102+
protected List<Long> zoneIds;
103+
93104
private String syncObjectType = BaseAsyncCmd.snapshotHostSyncObject;
94105

95106
// ///////////////////////////////////////////////////
@@ -148,6 +159,10 @@ private Long getHostId() {
148159
return _snapshotService.getHostIdForSnapshotOperation(volume);
149160
}
150161

162+
public List<Long> getZoneIds() {
163+
return zoneIds;
164+
}
165+
151166
// ///////////////////////////////////////////////////
152167
// ///////////// API Implementation///////////////////
153168
// ///////////////////////////////////////////////////
@@ -196,7 +211,7 @@ public ApiCommandResourceType getApiResourceType() {
196211

197212
@Override
198213
public void create() throws ResourceAllocationException {
199-
Snapshot snapshot = _volumeService.allocSnapshot(getVolumeId(), getPolicyId(), getSnapshotName(), getLocationType());
214+
Snapshot snapshot = _volumeService.allocSnapshot(getVolumeId(), getPolicyId(), getSnapshotName(), getLocationType(), getZoneIds());
200215
if (snapshot != null) {
201216
setEntityId(snapshot.getId());
202217
setEntityUuid(snapshot.getUuid());
@@ -210,7 +225,7 @@ public void execute() {
210225
Snapshot snapshot;
211226
try {
212227
snapshot =
213-
_volumeService.takeSnapshot(getVolumeId(), getPolicyId(), getEntityId(), _accountService.getAccount(getEntityOwnerId()), getQuiescevm(), getLocationType(), getAsyncBackup(), getTags());
228+
_volumeService.takeSnapshot(getVolumeId(), getPolicyId(), getEntityId(), _accountService.getAccount(getEntityOwnerId()), getQuiescevm(), getLocationType(), getAsyncBackup(), getTags(), getZoneIds());
214229

215230
if (snapshot != null) {
216231
SnapshotResponse response = _responseGenerator.createSnapshotResponse(snapshot);

api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public void execute() {
186186
} finally {
187187
if (snapshot == null) {
188188
try {
189-
_snapshotService.deleteSnapshot(getEntityId());
189+
_snapshotService.deleteSnapshot(getEntityId(), null);
190190
} catch (Exception e) {
191191
s_logger.debug("Failed to clean failed snapshot" + getEntityId());
192192
}

0 commit comments

Comments
 (0)