Skip to content

Commit

Permalink
support CopyStart for incremental snapshot (Azure#31861)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
XiaofeiCao and weidongxu-microsoft authored Nov 3, 2022
1 parent c90a861 commit 6e44704
Show file tree
Hide file tree
Showing 6 changed files with 1,322 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@

### Features Added

### Breaking Changes

### Bugs Fixed

### Other Changes
- Supported `withCopyStart` method in `Snapshot` for copying incremental snapshot from incremental snapshot.
- Supported `awaitCopyStartCompletion` and `awaitCopyStartCompletionAsync` method in `Snapshot`.
- Supported `copyCompletionPercent` and `copyCompletionError` method in `Snapshot` for retrieving `CopyStart` progress.

## 2.20.0 (2022-10-26)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

package com.azure.resourcemanager.compute.implementation;

import com.azure.core.management.exception.ManagementException;
import com.azure.core.util.logging.ClientLogger;
import com.azure.resourcemanager.compute.ComputeManager;
import com.azure.resourcemanager.compute.fluent.models.SnapshotInner;
import com.azure.resourcemanager.compute.models.AccessLevel;
import com.azure.resourcemanager.compute.models.CopyCompletionError;
import com.azure.resourcemanager.compute.models.CreationData;
import com.azure.resourcemanager.compute.models.CreationSource;
import com.azure.resourcemanager.compute.models.Disk;
Expand All @@ -15,12 +18,14 @@
import com.azure.resourcemanager.compute.models.Snapshot;
import com.azure.resourcemanager.compute.models.SnapshotSku;
import com.azure.resourcemanager.compute.models.SnapshotSkuType;
import com.azure.resourcemanager.compute.fluent.models.SnapshotInner;
import com.azure.resourcemanager.resources.fluentcore.arm.ResourceUtils;
import com.azure.resourcemanager.resources.fluentcore.arm.models.implementation.GroupableResourceImpl;
import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Objects;

/** The implementation for Snapshot and its create and update interfaces. */
class SnapshotImpl extends GroupableResourceImpl<Snapshot, SnapshotInner, SnapshotImpl, ComputeManager>
implements Snapshot, Snapshot.Definition, Snapshot.Update {
Expand Down Expand Up @@ -65,6 +70,16 @@ public CreationSource source() {
return new CreationSource(this.innerModel().creationData());
}

@Override
public Float copyCompletionPercent() {
return this.innerModel().completionPercent();
}

@Override
public CopyCompletionError copyCompletionError() {
return this.innerModel().copyCompletionError();
}

@Override
public String grantAccess(int accessDurationInSeconds) {
return this.grantAccessAsync(accessDurationInSeconds).block();
Expand All @@ -91,6 +106,52 @@ public Mono<Void> revokeAccessAsync() {
return this.manager().serviceClient().getSnapshots().revokeAccessAsync(this.resourceGroupName(), this.name());
}

@Override
public void awaitCopyStartCompletion() {
awaitCopyStartCompletionAsync().block();
}

@Override
public Boolean awaitCopyStartCompletion(Duration maxWaitTime) {
Objects.requireNonNull(maxWaitTime);
if (maxWaitTime.isNegative() || maxWaitTime.isZero()) {
throw new IllegalArgumentException(String.format("Max wait time is non-positive: %dms", maxWaitTime.toMillis()));
}
return this.awaitCopyStartCompletionAsync()
.then(Mono.just(Boolean.TRUE))
.timeout(maxWaitTime, Mono.just(Boolean.FALSE))
.block();
}

@Override
public Mono<Void> awaitCopyStartCompletionAsync() {
if (creationMethod() != DiskCreateOption.COPY_START) {
return Mono.error(logger.logThrowableAsError(new IllegalStateException(
String.format(
"\"awaitCopyStartCompletionAsync\" cannot be called on snapshot \"%s\" when \"creationMethod\" is not \"CopyStart\"", this.name()))));
}
return getInnerAsync()
.flatMap(inner -> {
setInner(inner);
Mono<SnapshotInner> result = Mono.just(inner);
if (inner.copyCompletionError() != null) { // service error
result = Mono.error(new ManagementException(inner.copyCompletionError().errorMessage(), null));
} else if (inner.completionPercent() == null || inner.completionPercent() != 100) { // in progress
logger.info("Wait for CopyStart complete for snapshot: {}. Complete percent: {}.",
inner.name(), inner.completionPercent());
result = Mono.empty();
}
return result;
})
.repeatWhenEmpty(longFlux ->
longFlux
.flatMap(
index ->
Mono.delay(ResourceManagerUtils.InternalRuntimeContext.getDelayDuration(
manager().serviceClient().getDefaultPollInterval()))))
.then();
}

@Override
public SnapshotImpl withLinuxFromVhd(String vhdUrl) {
return withLinuxFromVhd(vhdUrl, constructStorageAccountId(vhdUrl));
Expand Down Expand Up @@ -246,6 +307,14 @@ public SnapshotImpl withDataFromSnapshot(Snapshot snapshot) {
return withDataFromSnapshot(snapshot.id());
}

@Override
public SnapshotImpl withCopyStart() {
this.innerModel()
.creationData()
.withCreateOption(DiskCreateOption.COPY_START);
return this;
}

@Override
public SnapshotImpl withDataFromDisk(String managedDiskId) {
this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public CreationSourceType type() {
if (createOption == DiskCreateOption.IMPORT) {
return CreationSourceType.IMPORTED_FROM_VHD;
}
if (createOption == DiskCreateOption.COPY) {
if (createOption == DiskCreateOption.COPY || createOption == DiskCreateOption.COPY_START) {
String sourceResourceId = this.creationData.sourceResourceId();
if (sourceResourceId == null && this.creationData.sourceUri() != null) {
sourceResourceId = this.creationData.sourceUri();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import com.azure.resourcemanager.resources.fluentcore.model.Updatable;
import reactor.core.publisher.Mono;

import java.time.Duration;

/** An immutable client-side representation of an Azure managed snapshot. */
@Fluent
public interface Snapshot
Expand All @@ -36,6 +38,23 @@ public interface Snapshot
/** @return the details of the source from which snapshot is created */
CreationSource source();

/**
* Gets the percentage complete for the background copy when a resource is created via the CopyStart operation.
* <p>For latest progress,{@link Snapshot#refresh()} or {@link Snapshot#refreshAsync()} should be called prior to this method.</p>
*
* @return the percentage complete, ranging from 0 to 100, or null if {@link Snapshot#creationMethod()} is not {@link DiskCreateOption#COPY_START}
*/
Float copyCompletionPercent();

/**
* Gets the error details if the background copy of a resource created via the CopyStart operation fails.
* <p>For latest progress,{@link Snapshot#refresh()} or {@link Snapshot#refreshAsync()} should be called
* prior to this method. </p>
*
* @return the error details
*/
CopyCompletionError copyCompletionError();

/**
* Grants access to the snapshot.
*
Expand All @@ -62,6 +81,27 @@ public interface Snapshot
*/
Mono<Void> revokeAccessAsync();

/**
* Await CopyStart completion indefinitely unless errors are encountered.
*/
void awaitCopyStartCompletion();

/**
* Await CopyStart completion for a specified timeout.
*
* @param maxWaitTime max timeout to wait for completion
* @return true if CopyStart complete successfully, false if timeout
* @throws com.azure.core.management.exception.ManagementException if exceptions are encountered
*/
Boolean awaitCopyStartCompletion(Duration maxWaitTime);

/**
* Await CopyStart completion in async manner.
*
* @return a representation of the deferred computation of this call
*/
Mono<Void> awaitCopyStartCompletionAsync();

/** The entirety of the managed snapshot definition. */
interface Definition
extends DefinitionStages.Blank,
Expand Down Expand Up @@ -258,6 +298,24 @@ interface WithDataSnapshotFromSnapshot {
WithCreate withDataFromSnapshot(Snapshot snapshot);
}

/** The stage of the managed snapshot definition allowing to set creationOption to CopyStart. */
interface WithCopyStart {
/**
* Specifies CopyStart for CreateOption.
* <p>CopyStart can be used when source and target regions are different as well as when they are the same.
* There are important scenarios (copying across zones, copying from main region to edge location and other way around)
* where it is necessary to use CopyStart within the same region. </p>
* <p>Note: For now, CopyStart is only supported for creating an incremental snapshot from an incremental snapshot.</p>
* <p>Before you can use the copied snapshot for future use (e.g. create disk), you should wait for the CopyStart
* completion by calling {@link Snapshot#awaitCopyStartCompletion()} or {@link Snapshot#awaitCopyStartCompletion(Duration)}
* to wait synchronously, or {@link Snapshot#awaitCopyStartCompletionAsync()} to wait asynchronously.</p>
*
* @see DiskCreateOption
* @return the next stage of the definition
*/
WithCreate withCopyStart();
}

/** The stage of the managed disk definition allowing to choose a source operating system image. */
interface WithOSSnapshotFromImage {
/**
Expand Down Expand Up @@ -357,7 +415,8 @@ interface WithCreate
Resource.DefinitionWithTags<Snapshot.DefinitionStages.WithCreate>,
WithSize,
WithSku,
WithIncremental {
WithIncremental,
WithCopyStart {
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import com.azure.core.http.HttpPipeline;
import com.azure.core.http.rest.PagedIterable;
import com.azure.core.management.Region;
import com.azure.core.management.profile.AzureProfile;
import com.azure.resourcemanager.compute.models.CreationSourceType;
import com.azure.resourcemanager.compute.models.Disk;
import com.azure.resourcemanager.compute.models.DiskCreateOption;
Expand All @@ -13,14 +15,16 @@
import com.azure.resourcemanager.compute.models.SnapshotSkuType;
import com.azure.resourcemanager.resources.models.ResourceGroup;
import com.azure.resourcemanager.test.utils.TestUtilities;
import com.azure.core.management.Region;
import com.azure.core.management.profile.AzureProfile;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.time.Duration;

public class ManagedDiskOperationsTests extends ComputeManagementTest {
private String rgName = "";
private String rgName2 = null;
private Region region = Region.US_WEST_CENTRAL;
private Region region2 = Region.US_EAST;

@Override
protected void initializeClients(HttpPipeline httpPipeline, AzureProfile profile) {
Expand All @@ -31,6 +35,9 @@ protected void initializeClients(HttpPipeline httpPipeline, AzureProfile profile
@Override
protected void cleanUpResources() {
resourceManager.resourceGroups().beginDeleteByName(rgName);
if (rgName2 != null) {
resourceManager.resourceGroups().beginDeleteByName(rgName2);
}
}

@Test
Expand Down Expand Up @@ -237,4 +244,121 @@ public void canOperateOnManagedDiskFromSnapshot() {
Assertions.assertEquals(fromSnapshotDisk.source().type(), CreationSourceType.COPIED_FROM_SNAPSHOT);
Assertions.assertTrue(fromSnapshotDisk.source().sourceId().equalsIgnoreCase(snapshot.id()));
}

@Test
public void canCopyStartIncrementalSnapshot() {
rgName2 = generateRandomResourceName("rg", 15);
final String emptyDiskName = generateRandomResourceName("md-empty-", 20);
final String snapshotName = generateRandomResourceName("snp-", 20);
final String snapshotName2 = generateRandomResourceName("snp-", 20);
final String newRegionSnapshotName = generateRandomResourceName("snp-newregion-", 20);
final String snapshotBasedDiskName = generateRandomResourceName("md-snp-newregion-", 20);

ResourceGroup resourceGroup = resourceManager.resourceGroups().define(rgName).withRegion(region).create();
ResourceGroup resourceGroup2 = resourceManager.resourceGroups().define(rgName2).withRegion(region2).create();

// create disk to copy
Disk emptyDisk =
computeManager
.disks()
.define(emptyDiskName)
.withRegion(region)
.withExistingResourceGroup(resourceGroup)
.withData()
.withSizeInGB(100)
.create();

// create incremental snapshot from the disk
Snapshot snapshot =
computeManager
.snapshots()
.define(snapshotName)
.withRegion(region)
.withExistingResourceGroup(resourceGroup)
.withDataFromDisk(emptyDisk)
.withSku(SnapshotSkuType.STANDARD_LRS)
.withIncremental(true)
.create();

Assertions.assertTrue(snapshot.incremental());
Assertions.assertEquals(CreationSourceType.COPIED_FROM_DISK, snapshot.source().type());
Assertions.assertEquals(DiskCreateOption.COPY, snapshot.creationMethod());
Assertions.assertThrows(IllegalStateException.class, snapshot::awaitCopyStartCompletion);

// copy the snapshot to the same region
Snapshot snapshotSameRegion =
computeManager
.snapshots()
.define(snapshotName2)
.withRegion(region)
.withExistingResourceGroup(resourceGroup)
.withDataFromSnapshot(snapshot)
.withCopyStart()
.withIncremental(true)
.create();

Assertions.assertTrue(snapshotSameRegion.incremental());
Assertions.assertEquals(CreationSourceType.COPIED_FROM_SNAPSHOT, snapshotSameRegion.source().type());
Assertions.assertEquals(DiskCreateOption.COPY_START, snapshotSameRegion.creationMethod());
Assertions.assertNull(snapshotSameRegion.copyCompletionError());
// we don't wait for CopyStart to finish, so it should be in progress
Assertions.assertNotEquals(100, snapshotSameRegion.copyCompletionPercent());

computeManager
.snapshots()
.deleteById(snapshotSameRegion.id());

Snapshot snapshotSameRegion2 = computeManager
.snapshots()
.define(snapshotName2)
.withRegion(region)
.withExistingResourceGroup(resourceGroup)
.withDataFromSnapshot(snapshot)
.withCopyStart()
.withIncremental(true)
.create();
Assertions.assertFalse(snapshotSameRegion2.awaitCopyStartCompletion(Duration.ofMillis(1)));
Assertions.assertTrue(snapshotSameRegion2.awaitCopyStartCompletion(Duration.ofHours(24)));

// copy the snapshot to a new region
Snapshot snapshotNewRegion =
computeManager
.snapshots()
.define(newRegionSnapshotName)
.withRegion(region2)
.withExistingResourceGroup(resourceGroup2)
.withDataFromSnapshot(snapshot)
.withCopyStart()
.withIncremental(true)
.create();
snapshotNewRegion.awaitCopyStartCompletion();

Assertions.assertTrue(snapshotNewRegion.incremental());
Assertions.assertEquals(CreationSourceType.COPIED_FROM_SNAPSHOT, snapshotNewRegion.source().type());
Assertions.assertEquals(DiskCreateOption.COPY_START, snapshotNewRegion.creationMethod());
Assertions.assertEquals(100, snapshotNewRegion.copyCompletionPercent());
Assertions.assertNull(snapshotNewRegion.copyCompletionError());

// create disk from snapshot in the new region
Disk fromSnapshotDisk =
computeManager
.disks()
.define(snapshotBasedDiskName)
.withRegion(region2)
.withExistingResourceGroup(resourceGroup2)
.withData()
.fromSnapshot(snapshotNewRegion)
.withSizeInGB(300)
.create();

Assertions.assertNotNull(fromSnapshotDisk.id());
Assertions.assertTrue(fromSnapshotDisk.name().equalsIgnoreCase(snapshotBasedDiskName));
Assertions.assertEquals(fromSnapshotDisk.sku(), DiskSkuTypes.STANDARD_LRS);
Assertions.assertEquals(fromSnapshotDisk.creationMethod(), DiskCreateOption.COPY);
Assertions.assertEquals(fromSnapshotDisk.sizeInGB(), 300);
Assertions.assertNull(fromSnapshotDisk.osType());
Assertions.assertNotNull(fromSnapshotDisk.source());
Assertions.assertEquals(fromSnapshotDisk.source().type(), CreationSourceType.COPIED_FROM_SNAPSHOT);
Assertions.assertTrue(fromSnapshotDisk.source().sourceId().equalsIgnoreCase(snapshotNewRegion.id()));
}
}
Loading

0 comments on commit 6e44704

Please sign in to comment.