Skip to content

Commit 6e44704

Browse files
support CopyStart for incremental snapshot (Azure#31861)
* support CopyStart for incremental snapshot * checkstyle * fix test * refine javadoc * refine javadoc * nit, rename * change copy start implementation * Update sdk/resourcemanager/azure-resourcemanager-compute/CHANGELOG.md Co-authored-by: Weidong Xu <[email protected]> * add CopyStart check on awaitCopyStartCompletion * refine error message * refine error message Co-authored-by: Weidong Xu <[email protected]>
1 parent c90a861 commit 6e44704

File tree

6 files changed

+1322
-10
lines changed

6 files changed

+1322
-10
lines changed

sdk/resourcemanager/azure-resourcemanager-compute/CHANGELOG.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44

55
### Features Added
66

7-
### Breaking Changes
8-
9-
### Bugs Fixed
10-
11-
### Other Changes
7+
- Supported `withCopyStart` method in `Snapshot` for copying incremental snapshot from incremental snapshot.
8+
- Supported `awaitCopyStartCompletion` and `awaitCopyStartCompletionAsync` method in `Snapshot`.
9+
- Supported `copyCompletionPercent` and `copyCompletionError` method in `Snapshot` for retrieving `CopyStart` progress.
1210

1311
## 2.20.0 (2022-10-26)
1412

sdk/resourcemanager/azure-resourcemanager-compute/src/main/java/com/azure/resourcemanager/compute/implementation/SnapshotImpl.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33

44
package com.azure.resourcemanager.compute.implementation;
55

6+
import com.azure.core.management.exception.ManagementException;
67
import com.azure.core.util.logging.ClientLogger;
78
import com.azure.resourcemanager.compute.ComputeManager;
9+
import com.azure.resourcemanager.compute.fluent.models.SnapshotInner;
810
import com.azure.resourcemanager.compute.models.AccessLevel;
11+
import com.azure.resourcemanager.compute.models.CopyCompletionError;
912
import com.azure.resourcemanager.compute.models.CreationData;
1013
import com.azure.resourcemanager.compute.models.CreationSource;
1114
import com.azure.resourcemanager.compute.models.Disk;
@@ -15,12 +18,14 @@
1518
import com.azure.resourcemanager.compute.models.Snapshot;
1619
import com.azure.resourcemanager.compute.models.SnapshotSku;
1720
import com.azure.resourcemanager.compute.models.SnapshotSkuType;
18-
import com.azure.resourcemanager.compute.fluent.models.SnapshotInner;
1921
import com.azure.resourcemanager.resources.fluentcore.arm.ResourceUtils;
2022
import com.azure.resourcemanager.resources.fluentcore.arm.models.implementation.GroupableResourceImpl;
2123
import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils;
2224
import reactor.core.publisher.Mono;
2325

26+
import java.time.Duration;
27+
import java.util.Objects;
28+
2429
/** The implementation for Snapshot and its create and update interfaces. */
2530
class SnapshotImpl extends GroupableResourceImpl<Snapshot, SnapshotInner, SnapshotImpl, ComputeManager>
2631
implements Snapshot, Snapshot.Definition, Snapshot.Update {
@@ -65,6 +70,16 @@ public CreationSource source() {
6570
return new CreationSource(this.innerModel().creationData());
6671
}
6772

73+
@Override
74+
public Float copyCompletionPercent() {
75+
return this.innerModel().completionPercent();
76+
}
77+
78+
@Override
79+
public CopyCompletionError copyCompletionError() {
80+
return this.innerModel().copyCompletionError();
81+
}
82+
6883
@Override
6984
public String grantAccess(int accessDurationInSeconds) {
7085
return this.grantAccessAsync(accessDurationInSeconds).block();
@@ -91,6 +106,52 @@ public Mono<Void> revokeAccessAsync() {
91106
return this.manager().serviceClient().getSnapshots().revokeAccessAsync(this.resourceGroupName(), this.name());
92107
}
93108

109+
@Override
110+
public void awaitCopyStartCompletion() {
111+
awaitCopyStartCompletionAsync().block();
112+
}
113+
114+
@Override
115+
public Boolean awaitCopyStartCompletion(Duration maxWaitTime) {
116+
Objects.requireNonNull(maxWaitTime);
117+
if (maxWaitTime.isNegative() || maxWaitTime.isZero()) {
118+
throw new IllegalArgumentException(String.format("Max wait time is non-positive: %dms", maxWaitTime.toMillis()));
119+
}
120+
return this.awaitCopyStartCompletionAsync()
121+
.then(Mono.just(Boolean.TRUE))
122+
.timeout(maxWaitTime, Mono.just(Boolean.FALSE))
123+
.block();
124+
}
125+
126+
@Override
127+
public Mono<Void> awaitCopyStartCompletionAsync() {
128+
if (creationMethod() != DiskCreateOption.COPY_START) {
129+
return Mono.error(logger.logThrowableAsError(new IllegalStateException(
130+
String.format(
131+
"\"awaitCopyStartCompletionAsync\" cannot be called on snapshot \"%s\" when \"creationMethod\" is not \"CopyStart\"", this.name()))));
132+
}
133+
return getInnerAsync()
134+
.flatMap(inner -> {
135+
setInner(inner);
136+
Mono<SnapshotInner> result = Mono.just(inner);
137+
if (inner.copyCompletionError() != null) { // service error
138+
result = Mono.error(new ManagementException(inner.copyCompletionError().errorMessage(), null));
139+
} else if (inner.completionPercent() == null || inner.completionPercent() != 100) { // in progress
140+
logger.info("Wait for CopyStart complete for snapshot: {}. Complete percent: {}.",
141+
inner.name(), inner.completionPercent());
142+
result = Mono.empty();
143+
}
144+
return result;
145+
})
146+
.repeatWhenEmpty(longFlux ->
147+
longFlux
148+
.flatMap(
149+
index ->
150+
Mono.delay(ResourceManagerUtils.InternalRuntimeContext.getDelayDuration(
151+
manager().serviceClient().getDefaultPollInterval()))))
152+
.then();
153+
}
154+
94155
@Override
95156
public SnapshotImpl withLinuxFromVhd(String vhdUrl) {
96157
return withLinuxFromVhd(vhdUrl, constructStorageAccountId(vhdUrl));
@@ -246,6 +307,14 @@ public SnapshotImpl withDataFromSnapshot(Snapshot snapshot) {
246307
return withDataFromSnapshot(snapshot.id());
247308
}
248309

310+
@Override
311+
public SnapshotImpl withCopyStart() {
312+
this.innerModel()
313+
.creationData()
314+
.withCreateOption(DiskCreateOption.COPY_START);
315+
return this;
316+
}
317+
249318
@Override
250319
public SnapshotImpl withDataFromDisk(String managedDiskId) {
251320
this

sdk/resourcemanager/azure-resourcemanager-compute/src/main/java/com/azure/resourcemanager/compute/models/CreationSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public CreationSourceType type() {
3131
if (createOption == DiskCreateOption.IMPORT) {
3232
return CreationSourceType.IMPORTED_FROM_VHD;
3333
}
34-
if (createOption == DiskCreateOption.COPY) {
34+
if (createOption == DiskCreateOption.COPY || createOption == DiskCreateOption.COPY_START) {
3535
String sourceResourceId = this.creationData.sourceResourceId();
3636
if (sourceResourceId == null && this.creationData.sourceUri() != null) {
3737
sourceResourceId = this.creationData.sourceUri();

sdk/resourcemanager/azure-resourcemanager-compute/src/main/java/com/azure/resourcemanager/compute/models/Snapshot.java

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import com.azure.resourcemanager.resources.fluentcore.model.Updatable;
1515
import reactor.core.publisher.Mono;
1616

17+
import java.time.Duration;
18+
1719
/** An immutable client-side representation of an Azure managed snapshot. */
1820
@Fluent
1921
public interface Snapshot
@@ -36,6 +38,23 @@ public interface Snapshot
3638
/** @return the details of the source from which snapshot is created */
3739
CreationSource source();
3840

41+
/**
42+
* Gets the percentage complete for the background copy when a resource is created via the CopyStart operation.
43+
* <p>For latest progress,{@link Snapshot#refresh()} or {@link Snapshot#refreshAsync()} should be called prior to this method.</p>
44+
*
45+
* @return the percentage complete, ranging from 0 to 100, or null if {@link Snapshot#creationMethod()} is not {@link DiskCreateOption#COPY_START}
46+
*/
47+
Float copyCompletionPercent();
48+
49+
/**
50+
* Gets the error details if the background copy of a resource created via the CopyStart operation fails.
51+
* <p>For latest progress,{@link Snapshot#refresh()} or {@link Snapshot#refreshAsync()} should be called
52+
* prior to this method. </p>
53+
*
54+
* @return the error details
55+
*/
56+
CopyCompletionError copyCompletionError();
57+
3958
/**
4059
* Grants access to the snapshot.
4160
*
@@ -62,6 +81,27 @@ public interface Snapshot
6281
*/
6382
Mono<Void> revokeAccessAsync();
6483

84+
/**
85+
* Await CopyStart completion indefinitely unless errors are encountered.
86+
*/
87+
void awaitCopyStartCompletion();
88+
89+
/**
90+
* Await CopyStart completion for a specified timeout.
91+
*
92+
* @param maxWaitTime max timeout to wait for completion
93+
* @return true if CopyStart complete successfully, false if timeout
94+
* @throws com.azure.core.management.exception.ManagementException if exceptions are encountered
95+
*/
96+
Boolean awaitCopyStartCompletion(Duration maxWaitTime);
97+
98+
/**
99+
* Await CopyStart completion in async manner.
100+
*
101+
* @return a representation of the deferred computation of this call
102+
*/
103+
Mono<Void> awaitCopyStartCompletionAsync();
104+
65105
/** The entirety of the managed snapshot definition. */
66106
interface Definition
67107
extends DefinitionStages.Blank,
@@ -258,6 +298,24 @@ interface WithDataSnapshotFromSnapshot {
258298
WithCreate withDataFromSnapshot(Snapshot snapshot);
259299
}
260300

301+
/** The stage of the managed snapshot definition allowing to set creationOption to CopyStart. */
302+
interface WithCopyStart {
303+
/**
304+
* Specifies CopyStart for CreateOption.
305+
* <p>CopyStart can be used when source and target regions are different as well as when they are the same.
306+
* There are important scenarios (copying across zones, copying from main region to edge location and other way around)
307+
* where it is necessary to use CopyStart within the same region. </p>
308+
* <p>Note: For now, CopyStart is only supported for creating an incremental snapshot from an incremental snapshot.</p>
309+
* <p>Before you can use the copied snapshot for future use (e.g. create disk), you should wait for the CopyStart
310+
* completion by calling {@link Snapshot#awaitCopyStartCompletion()} or {@link Snapshot#awaitCopyStartCompletion(Duration)}
311+
* to wait synchronously, or {@link Snapshot#awaitCopyStartCompletionAsync()} to wait asynchronously.</p>
312+
*
313+
* @see DiskCreateOption
314+
* @return the next stage of the definition
315+
*/
316+
WithCreate withCopyStart();
317+
}
318+
261319
/** The stage of the managed disk definition allowing to choose a source operating system image. */
262320
interface WithOSSnapshotFromImage {
263321
/**
@@ -357,7 +415,8 @@ interface WithCreate
357415
Resource.DefinitionWithTags<Snapshot.DefinitionStages.WithCreate>,
358416
WithSize,
359417
WithSku,
360-
WithIncremental {
418+
WithIncremental,
419+
WithCopyStart {
361420
}
362421
}
363422

sdk/resourcemanager/azure-resourcemanager-compute/src/test/java/com/azure/resourcemanager/compute/ManagedDiskOperationsTests.java

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import com.azure.core.http.HttpPipeline;
77
import com.azure.core.http.rest.PagedIterable;
8+
import com.azure.core.management.Region;
9+
import com.azure.core.management.profile.AzureProfile;
810
import com.azure.resourcemanager.compute.models.CreationSourceType;
911
import com.azure.resourcemanager.compute.models.Disk;
1012
import com.azure.resourcemanager.compute.models.DiskCreateOption;
@@ -13,14 +15,16 @@
1315
import com.azure.resourcemanager.compute.models.SnapshotSkuType;
1416
import com.azure.resourcemanager.resources.models.ResourceGroup;
1517
import com.azure.resourcemanager.test.utils.TestUtilities;
16-
import com.azure.core.management.Region;
17-
import com.azure.core.management.profile.AzureProfile;
1818
import org.junit.jupiter.api.Assertions;
1919
import org.junit.jupiter.api.Test;
2020

21+
import java.time.Duration;
22+
2123
public class ManagedDiskOperationsTests extends ComputeManagementTest {
2224
private String rgName = "";
25+
private String rgName2 = null;
2326
private Region region = Region.US_WEST_CENTRAL;
27+
private Region region2 = Region.US_EAST;
2428

2529
@Override
2630
protected void initializeClients(HttpPipeline httpPipeline, AzureProfile profile) {
@@ -31,6 +35,9 @@ protected void initializeClients(HttpPipeline httpPipeline, AzureProfile profile
3135
@Override
3236
protected void cleanUpResources() {
3337
resourceManager.resourceGroups().beginDeleteByName(rgName);
38+
if (rgName2 != null) {
39+
resourceManager.resourceGroups().beginDeleteByName(rgName2);
40+
}
3441
}
3542

3643
@Test
@@ -237,4 +244,121 @@ public void canOperateOnManagedDiskFromSnapshot() {
237244
Assertions.assertEquals(fromSnapshotDisk.source().type(), CreationSourceType.COPIED_FROM_SNAPSHOT);
238245
Assertions.assertTrue(fromSnapshotDisk.source().sourceId().equalsIgnoreCase(snapshot.id()));
239246
}
247+
248+
@Test
249+
public void canCopyStartIncrementalSnapshot() {
250+
rgName2 = generateRandomResourceName("rg", 15);
251+
final String emptyDiskName = generateRandomResourceName("md-empty-", 20);
252+
final String snapshotName = generateRandomResourceName("snp-", 20);
253+
final String snapshotName2 = generateRandomResourceName("snp-", 20);
254+
final String newRegionSnapshotName = generateRandomResourceName("snp-newregion-", 20);
255+
final String snapshotBasedDiskName = generateRandomResourceName("md-snp-newregion-", 20);
256+
257+
ResourceGroup resourceGroup = resourceManager.resourceGroups().define(rgName).withRegion(region).create();
258+
ResourceGroup resourceGroup2 = resourceManager.resourceGroups().define(rgName2).withRegion(region2).create();
259+
260+
// create disk to copy
261+
Disk emptyDisk =
262+
computeManager
263+
.disks()
264+
.define(emptyDiskName)
265+
.withRegion(region)
266+
.withExistingResourceGroup(resourceGroup)
267+
.withData()
268+
.withSizeInGB(100)
269+
.create();
270+
271+
// create incremental snapshot from the disk
272+
Snapshot snapshot =
273+
computeManager
274+
.snapshots()
275+
.define(snapshotName)
276+
.withRegion(region)
277+
.withExistingResourceGroup(resourceGroup)
278+
.withDataFromDisk(emptyDisk)
279+
.withSku(SnapshotSkuType.STANDARD_LRS)
280+
.withIncremental(true)
281+
.create();
282+
283+
Assertions.assertTrue(snapshot.incremental());
284+
Assertions.assertEquals(CreationSourceType.COPIED_FROM_DISK, snapshot.source().type());
285+
Assertions.assertEquals(DiskCreateOption.COPY, snapshot.creationMethod());
286+
Assertions.assertThrows(IllegalStateException.class, snapshot::awaitCopyStartCompletion);
287+
288+
// copy the snapshot to the same region
289+
Snapshot snapshotSameRegion =
290+
computeManager
291+
.snapshots()
292+
.define(snapshotName2)
293+
.withRegion(region)
294+
.withExistingResourceGroup(resourceGroup)
295+
.withDataFromSnapshot(snapshot)
296+
.withCopyStart()
297+
.withIncremental(true)
298+
.create();
299+
300+
Assertions.assertTrue(snapshotSameRegion.incremental());
301+
Assertions.assertEquals(CreationSourceType.COPIED_FROM_SNAPSHOT, snapshotSameRegion.source().type());
302+
Assertions.assertEquals(DiskCreateOption.COPY_START, snapshotSameRegion.creationMethod());
303+
Assertions.assertNull(snapshotSameRegion.copyCompletionError());
304+
// we don't wait for CopyStart to finish, so it should be in progress
305+
Assertions.assertNotEquals(100, snapshotSameRegion.copyCompletionPercent());
306+
307+
computeManager
308+
.snapshots()
309+
.deleteById(snapshotSameRegion.id());
310+
311+
Snapshot snapshotSameRegion2 = computeManager
312+
.snapshots()
313+
.define(snapshotName2)
314+
.withRegion(region)
315+
.withExistingResourceGroup(resourceGroup)
316+
.withDataFromSnapshot(snapshot)
317+
.withCopyStart()
318+
.withIncremental(true)
319+
.create();
320+
Assertions.assertFalse(snapshotSameRegion2.awaitCopyStartCompletion(Duration.ofMillis(1)));
321+
Assertions.assertTrue(snapshotSameRegion2.awaitCopyStartCompletion(Duration.ofHours(24)));
322+
323+
// copy the snapshot to a new region
324+
Snapshot snapshotNewRegion =
325+
computeManager
326+
.snapshots()
327+
.define(newRegionSnapshotName)
328+
.withRegion(region2)
329+
.withExistingResourceGroup(resourceGroup2)
330+
.withDataFromSnapshot(snapshot)
331+
.withCopyStart()
332+
.withIncremental(true)
333+
.create();
334+
snapshotNewRegion.awaitCopyStartCompletion();
335+
336+
Assertions.assertTrue(snapshotNewRegion.incremental());
337+
Assertions.assertEquals(CreationSourceType.COPIED_FROM_SNAPSHOT, snapshotNewRegion.source().type());
338+
Assertions.assertEquals(DiskCreateOption.COPY_START, snapshotNewRegion.creationMethod());
339+
Assertions.assertEquals(100, snapshotNewRegion.copyCompletionPercent());
340+
Assertions.assertNull(snapshotNewRegion.copyCompletionError());
341+
342+
// create disk from snapshot in the new region
343+
Disk fromSnapshotDisk =
344+
computeManager
345+
.disks()
346+
.define(snapshotBasedDiskName)
347+
.withRegion(region2)
348+
.withExistingResourceGroup(resourceGroup2)
349+
.withData()
350+
.fromSnapshot(snapshotNewRegion)
351+
.withSizeInGB(300)
352+
.create();
353+
354+
Assertions.assertNotNull(fromSnapshotDisk.id());
355+
Assertions.assertTrue(fromSnapshotDisk.name().equalsIgnoreCase(snapshotBasedDiskName));
356+
Assertions.assertEquals(fromSnapshotDisk.sku(), DiskSkuTypes.STANDARD_LRS);
357+
Assertions.assertEquals(fromSnapshotDisk.creationMethod(), DiskCreateOption.COPY);
358+
Assertions.assertEquals(fromSnapshotDisk.sizeInGB(), 300);
359+
Assertions.assertNull(fromSnapshotDisk.osType());
360+
Assertions.assertNotNull(fromSnapshotDisk.source());
361+
Assertions.assertEquals(fromSnapshotDisk.source().type(), CreationSourceType.COPIED_FROM_SNAPSHOT);
362+
Assertions.assertTrue(fromSnapshotDisk.source().sourceId().equalsIgnoreCase(snapshotNewRegion.id()));
363+
}
240364
}

0 commit comments

Comments
 (0)