
com.vmware.photon.controller.model.adapters.azure.instance.AzureInstanceService Maven / Gradle / Ivy
/*
* Copyright (c) 2015-2017 VMware, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with 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.vmware.photon.controller.model.adapters.azure.instance;
import static com.vmware.photon.controller.model.ComputeProperties.RESOURCE_GROUP_NAME;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.AZURE_CORE_MANAGEMENT_URI;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.AZURE_DATA_DISK_CACHING;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.AZURE_OSDISK_BLOB_URI;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.AZURE_OSDISK_CACHING;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.AZURE_STORAGE_ACCOUNT_DEFAULT_RG_NAME;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.AZURE_STORAGE_ACCOUNT_NAME;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.AZURE_STORAGE_ACCOUNT_RG_NAME;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.COMPUTE_NAMESPACE;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.INVALID_PARAMETER;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.INVALID_RESOURCE_GROUP;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.MISSING_SUBSCRIPTION_CODE;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.NETWORK_NAMESPACE;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.PROVIDER_REGISTRED_STATE;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.PROVISIONING_STATE_SUCCEEDED;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.RESOURCE_GROUP_NOT_FOUND;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.STORAGE_ACCOUNT_ALREADY_EXIST;
import static com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants.STORAGE_NAMESPACE;
import static com.vmware.photon.controller.model.adapters.azure.utils.AzureUtils.getStorageAccountKeyName;
import static com.vmware.photon.controller.model.constants.PhotonModelConstants.CLOUD_CONFIG_DEFAULT_FILE_INDEX;
import static com.vmware.xenon.common.Operation.STATUS_CODE_UNAUTHORIZED;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.microsoft.azure.CloudError;
import com.microsoft.azure.CloudException;
import com.microsoft.azure.SubResource;
import com.microsoft.azure.credentials.ApplicationTokenCredentials;
import com.microsoft.azure.management.compute.CachingTypes;
import com.microsoft.azure.management.compute.DataDisk;
import com.microsoft.azure.management.compute.DiskCreateOptionTypes;
import com.microsoft.azure.management.compute.HardwareProfile;
import com.microsoft.azure.management.compute.NetworkProfile;
import com.microsoft.azure.management.compute.OSDisk;
import com.microsoft.azure.management.compute.OSProfile;
import com.microsoft.azure.management.compute.OperatingSystemTypes;
import com.microsoft.azure.management.compute.StorageProfile;
import com.microsoft.azure.management.compute.VirtualHardDisk;
import com.microsoft.azure.management.compute.VirtualMachineSizeTypes;
import com.microsoft.azure.management.compute.implementation.ComputeManagementClientImpl;
import com.microsoft.azure.management.compute.implementation.ImageReferenceInner;
import com.microsoft.azure.management.compute.implementation.NetworkInterfaceReferenceInner;
import com.microsoft.azure.management.compute.implementation.VirtualMachineImageResourceInner;
import com.microsoft.azure.management.compute.implementation.VirtualMachineInner;
import com.microsoft.azure.management.network.AddressSpace;
import com.microsoft.azure.management.network.IPAllocationMethod;
import com.microsoft.azure.management.network.implementation.NetworkInterfaceIPConfigurationInner;
import com.microsoft.azure.management.network.implementation.NetworkInterfaceInner;
import com.microsoft.azure.management.network.implementation.NetworkInterfacesInner;
import com.microsoft.azure.management.network.implementation.NetworkManagementClientImpl;
import com.microsoft.azure.management.network.implementation.NetworkSecurityGroupInner;
import com.microsoft.azure.management.network.implementation.NetworkSecurityGroupsInner;
import com.microsoft.azure.management.network.implementation.PublicIPAddressInner;
import com.microsoft.azure.management.network.implementation.PublicIPAddressesInner;
import com.microsoft.azure.management.network.implementation.SubnetInner;
import com.microsoft.azure.management.network.implementation.VirtualNetworkInner;
import com.microsoft.azure.management.network.implementation.VirtualNetworksInner;
import com.microsoft.azure.management.resources.implementation.ProviderInner;
import com.microsoft.azure.management.resources.implementation.ResourceGroupInner;
import com.microsoft.azure.management.resources.implementation.ResourceGroupsInner;
import com.microsoft.azure.management.resources.implementation.ResourceManagementClientImpl;
import com.microsoft.azure.management.resources.implementation.SubscriptionClientImpl;
import com.microsoft.azure.management.resources.implementation.SubscriptionInner;
import com.microsoft.azure.management.storage.Kind;
import com.microsoft.azure.management.storage.ProvisioningState;
import com.microsoft.azure.management.storage.Sku;
import com.microsoft.azure.management.storage.SkuName;
import com.microsoft.azure.management.storage.StorageAccountKey;
import com.microsoft.azure.management.storage.implementation.StorageAccountCreateParametersInner;
import com.microsoft.azure.management.storage.implementation.StorageAccountInner;
import com.microsoft.azure.management.storage.implementation.StorageAccountListKeysResultInner;
import com.microsoft.azure.management.storage.implementation.StorageManagementClientImpl;
import com.microsoft.rest.ServiceCallback;
import com.vmware.photon.controller.model.adapterapi.ComputeInstanceRequest;
import com.vmware.photon.controller.model.adapterapi.ComputeInstanceRequest.InstanceRequestType;
import com.vmware.photon.controller.model.adapters.azure.AzureAsyncCallback;
import com.vmware.photon.controller.model.adapters.azure.AzureUriPaths;
import com.vmware.photon.controller.model.adapters.azure.constants.AzureConstants;
import com.vmware.photon.controller.model.adapters.azure.instance.AzureInstanceContext.AzureNicContext;
import com.vmware.photon.controller.model.adapters.azure.model.diagnostics.AzureDiagnosticSettings;
import com.vmware.photon.controller.model.adapters.azure.utils.AzureDecommissionCallback;
import com.vmware.photon.controller.model.adapters.azure.utils.AzureDeferredResultServiceCallback;
import com.vmware.photon.controller.model.adapters.azure.utils.AzureProvisioningCallback;
import com.vmware.photon.controller.model.adapters.azure.utils.AzureSdkClients;
import com.vmware.photon.controller.model.adapters.azure.utils.AzureSecurityGroupUtils;
import com.vmware.photon.controller.model.adapters.azure.utils.AzureUtils;
import com.vmware.photon.controller.model.adapters.util.AdapterUtils;
import com.vmware.photon.controller.model.adapters.util.BaseAdapterContext.BaseAdapterStage;
import com.vmware.photon.controller.model.adapters.util.instance.BaseComputeInstanceContext.ImageSource;
import com.vmware.photon.controller.model.resources.ComputeDescriptionService;
import com.vmware.photon.controller.model.resources.ComputeDescriptionService.ComputeDescription;
import com.vmware.photon.controller.model.resources.ComputeDescriptionService.ComputeDescription.ComputeType;
import com.vmware.photon.controller.model.resources.ComputeService.ComputeState;
import com.vmware.photon.controller.model.resources.ComputeService.LifecycleState;
import com.vmware.photon.controller.model.resources.DiskService;
import com.vmware.photon.controller.model.resources.DiskService.DiskState;
import com.vmware.photon.controller.model.resources.ImageService.ImageState;
import com.vmware.photon.controller.model.resources.ImageService.ImageState.DiskConfiguration;
import com.vmware.photon.controller.model.resources.NetworkInterfaceDescriptionService.IpAssignment;
import com.vmware.photon.controller.model.resources.NetworkInterfaceDescriptionService.NetworkInterfaceDescription;
import com.vmware.photon.controller.model.resources.NetworkInterfaceService.NetworkInterfaceState;
import com.vmware.photon.controller.model.resources.SecurityGroupService.SecurityGroupState;
import com.vmware.photon.controller.model.resources.StorageDescriptionService;
import com.vmware.photon.controller.model.resources.StorageDescriptionService.StorageDescription;
import com.vmware.photon.controller.model.security.util.EncryptionUtils;
import com.vmware.xenon.common.DeferredResult;
import com.vmware.xenon.common.FileUtils;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.OperationJoin;
import com.vmware.xenon.common.ServiceErrorResponse;
import com.vmware.xenon.common.StatelessService;
import com.vmware.xenon.common.UriUtils;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.services.common.AuthCredentialsService;
import com.vmware.xenon.services.common.AuthCredentialsService.AuthCredentialsServiceState;
/**
* Adapter to create/delete a VM instance on Azure.
*/
public class AzureInstanceService extends StatelessService {
public static final String SELF_LINK = AzureUriPaths.AZURE_INSTANCE_ADAPTER;
// TODO VSYM-322: Remove unused default properties from AzureInstanceService
// Name prefixes
private static final String NICCONFIG_NAME_PREFIX = "nicconfig";
private static final String DEFAULT_GROUP_PREFIX = "group";
private static final String VHD_URI_FORMAT = "https://%s.blob.core.windows.net/vhds/%s.vhd";
private static final Pattern VHD_URI_PATTERN = Pattern
.compile("https://([\\p{Lower}\\p{Digit}]+).blob.core.windows.net/(.+)\\.vhd");
private static final String BOOT_DISK_SUFFIX = "-boot-disk";
private static final String DATA_DISK_SUFFIX = "-data-disk";
private static final long DEFAULT_EXPIRATION_INTERVAL_MICROS = TimeUnit.MINUTES.toMicros(5);
private static final int RETRY_INTERVAL_SECONDS = 30;
private static final long AZURE_MAXIMUM_OS_DISK_SIZE_MB = 1023 * 1024; // Maximum allowed OS
// disk size on Azure is 1023 GB
private ExecutorService executorService;
/**
* The class represents the context of async calls(either single or batch) to Azure cloud.
*
* @see TransitionToCallback
*/
static class AzureCallContext {
/**
* The number of calls associated with this context.
*/
final AtomicInteger numberOfCalls;
/**
* Flag indicating whether any call has failed.
*/
final AtomicBoolean hasAnyFailed = new AtomicBoolean(false);
/**
* Flag indicating whether Azure error should be considered as exceptional. Default behavior
* is to fail on error.
*/
boolean failOnError = true;
private AzureCallContext(int numberOfCalls) {
this.numberOfCalls = new AtomicInteger(numberOfCalls);
}
static AzureCallContext newSingleCallContext() {
return new AzureCallContext(1);
}
static AzureCallContext newBatchCallContext(int numberOfCalls) {
return new AzureCallContext(numberOfCalls);
}
}
@Override
public void handleStart(Operation startPost) {
this.executorService = getHost().allocateExecutor(this);
super.handleStart(startPost);
}
@Override
public void handleStop(Operation delete) {
this.executorService.shutdown();
AdapterUtils.awaitTermination(this.executorService);
super.handleStop(delete);
}
@Override
public void handlePatch(Operation op) {
if (!op.hasBody()) {
op.fail(new IllegalArgumentException("body is required"));
return;
}
AzureInstanceContext ctx = new AzureInstanceContext(this,
op.getBody(ComputeInstanceRequest.class));
final BaseAdapterStage startingStage;
switch (ctx.computeRequest.requestType) {
case VALIDATE_CREDENTIALS:
ctx.operation = op;
startingStage = BaseAdapterStage.PARENTAUTH;
break;
default:
op.complete();
if (ctx.computeRequest.isMockRequest
&& ctx.computeRequest.requestType == InstanceRequestType.CREATE) {
handleAllocation(ctx, AzureInstanceStage.FINISHED);
return;
}
startingStage = BaseAdapterStage.VMDESC;
break;
}
// Populate BaseAdapterContext and then continue with this state machine
ctx.populateBaseContext(startingStage)
.whenComplete(thenAllocation(ctx, AzureInstanceStage.CLIENT));
}
/**
* Shortcut method that sets the next stage into the context and delegates to
* {@link #handleAllocation(AzureInstanceContext)}.
*/
private void handleAllocation(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
logFine(() -> "Transition to " + nextStage);
ctx.stage = nextStage;
handleAllocation(ctx);
}
/**
* Shortcut method that stores the error into context, sets next stage to
* {@link AzureInstanceStage#ERROR} and delegates to
* {@link #handleAllocation(AzureInstanceContext)}.
*/
private void handleError(AzureInstanceContext ctx, Throwable e) {
ctx.error = e;
handleAllocation(ctx, AzureInstanceStage.ERROR);
}
/**
* {@code handleAllocation} version suitable for chaining to {@code DeferredResult.whenComplete}
* .
*/
private BiConsumer thenAllocation(AzureInstanceContext ctx,
AzureInstanceStage next, String namespace) {
return (ignoreCtx, exc) -> {
// NOTE: In case of error 'ignoreCtx' is null so use passed context!
if (exc != null) {
if (namespace != null) {
handleCloudError(String.format("%s: FAILED. Details:", ctx.stage), ctx,
namespace, exc);
} else {
handleError(ctx, exc);
}
} else {
handleAllocation(ctx, next);
}
};
}
private BiConsumer thenAllocation(AzureInstanceContext ctx,
AzureInstanceStage next) {
return thenAllocation(ctx, next, null);
}
/**
* State machine to handle different stages of VM creation/deletion.
*
* @see #handleError(AzureInstanceContext, Throwable)
* @see #handleAllocation(AzureInstanceContext, AzureInstanceStage)
*/
private void handleAllocation(AzureInstanceContext ctx) {
logInfo("Azure instance management at stage %s", ctx.stage);
try {
switch (ctx.stage) {
case CLIENT:
if (ctx.azureSdkClients == null) {
ctx.azureSdkClients = new AzureSdkClients(this.executorService, ctx.parentAuth);
}
switch (ctx.computeRequest.requestType) {
case CREATE:
handleAllocation(ctx, AzureInstanceStage.CHILDAUTH);
break;
case VALIDATE_CREDENTIALS:
validateAzureCredentials(ctx);
break;
case DELETE:
handleAllocation(ctx, AzureInstanceStage.DELETE);
break;
default:
throw new IllegalStateException("Unknown compute request type: " +
ctx.computeRequest.requestType);
}
break;
case CHILDAUTH:
getChildAuth(ctx, AzureInstanceStage.VMDISKS);
break;
case VMDISKS:
getVMDisks(ctx, AzureInstanceStage.INIT_RES_GROUP);
break;
case INIT_RES_GROUP:
createResourceGroup(ctx, AzureInstanceStage.GET_IMAGE);
break;
case GET_IMAGE:
getImageSource(ctx)
.whenComplete(thenAllocation(ctx, AzureInstanceStage.INIT_STORAGE));
break;
case INIT_STORAGE:
createStorageAccount(ctx, AzureInstanceStage.POPULATE_NIC_CONTEXT);
break;
case POPULATE_NIC_CONTEXT:
ctx.populateContext()
.whenComplete(thenAllocation(ctx, AzureInstanceStage.CREATE_NETWORKS));
break;
case CREATE_NETWORKS:
// Create Azure networks, PIPs and NSGs referred by NIC states
createNetworkIfNotExist(ctx, AzureInstanceStage.CREATE_PUBLIC_IPS);
break;
case CREATE_PUBLIC_IPS:
createPublicIPs(ctx, AzureInstanceStage.CREATE_SECURITY_GROUPS);
break;
case CREATE_SECURITY_GROUPS:
createSecurityGroupsIfNotExist(ctx, AzureInstanceStage.CREATE_NICS);
break;
case CREATE_NICS:
createNICs(ctx, AzureInstanceStage.GENERATE_VM_ID);
break;
case GENERATE_VM_ID:
generateVmId(ctx, AzureInstanceStage.CREATE);
break;
case CREATE:
// Finally provision the VM
createVM(ctx, AzureInstanceStage.UPDATE_COMPUTE_STATE_DETAILS);
break;
case ENABLE_MONITORING:
// TODO VSYM-620: Enable monitoring on Azure VMs
enableMonitoring(ctx, AzureInstanceStage.GET_STORAGE_KEYS);
break;
case UPDATE_COMPUTE_STATE_DETAILS:
updateComputeStateDetails(ctx, AzureInstanceStage.GET_STORAGE_KEYS);
break;
case GET_STORAGE_KEYS:
getStorageKeys(ctx, AzureInstanceStage.FINISHED);
break;
case DELETE:
deleteVM(ctx);
break;
case FINISHED:
// This is the ultimate exit point with success of the state machine
finishWithSuccess(ctx);
break;
case ERROR:
// This is the ultimate exit point with error of the state machine
errorHandler(ctx);
break;
default:
throw new IllegalStateException("Unknown stage: " + ctx.stage);
}
} catch (Throwable e) {
// NOTE: Do not use handleError(err) cause that might result in endless recursion.
ctx.error = e;
errorHandler(ctx);
}
}
/**
* Validates azure credential by making an API call.
*/
private void validateAzureCredentials(final AzureInstanceContext ctx) {
if (ctx.computeRequest.isMockRequest) {
ctx.operation.complete();
return;
}
SubscriptionClientImpl subscriptionClient = new SubscriptionClientImpl(
ctx.azureSdkClients.credentials);
subscriptionClient.subscriptions().getAsync(
ctx.parentAuth.userLink, new ServiceCallback() {
@Override
public void failure(Throwable e) {
// Azure doesn't send us any meaningful status code to work with
ServiceErrorResponse rsp = new ServiceErrorResponse();
rsp.message = "Invalid Azure credentials";
rsp.statusCode = STATUS_CODE_UNAUTHORIZED;
ctx.operation.fail(e, rsp);
}
@Override
public void success(SubscriptionInner result) {
logFine(() -> String.format("Got subscription %s with id %s",
result.displayName(), result.id()));
ctx.operation.complete();
}
});
}
private void deleteVM(AzureInstanceContext ctx) {
if (ctx.computeRequest.isMockRequest) {
handleAllocation(ctx, AzureInstanceStage.FINISHED);
return;
}
String rgName = getResourceGroupName(ctx);
if (rgName == null || rgName.isEmpty()) {
throw new IllegalArgumentException("Resource group name is required");
}
ResourceGroupsInner azureClient = getResourceManagementClientImpl(ctx)
.resourceGroups();
String msg = "Deleting resource group [" + rgName + "] for [" + ctx.vmName + "] VM";
AzureDeferredResultServiceCallback callback = new AzureDeferredResultServiceCallback(
this, msg) {
@Override
protected Throwable consumeError(Throwable exc) {
exc = super.consumeError(exc);
if (exc instanceof CloudException) {
CloudException azureExc = (CloudException) exc;
CloudError body = azureExc.body();
String code = body.code();
if (RESOURCE_GROUP_NOT_FOUND.equals(code)) {
return RECOVERED;
} else if (INVALID_RESOURCE_GROUP.equals(code)) {
String invalidParameterMsg = String.format(
"Invalid resource group parameter. %s",
body.message());
return new IllegalStateException(invalidParameterMsg, exc);
}
}
return exc;
}
@Override
protected DeferredResult consumeSuccess(Void body) {
return DeferredResult.completed(body);
}
};
azureClient.deleteAsync(rgName, callback);
callback.toDeferredResult()
.thenApply(ignore -> ctx)
.whenComplete(thenAllocation(ctx, AzureInstanceStage.FINISHED));
}
/**
* The ultimate error handler that should handle errors from all sources.
*
* NOTE: Do not use directly. Use it through
* {@link #handleError(AzureInstanceContext, Throwable)}.
*/
private void errorHandler(AzureInstanceContext ctx) {
logSevere(ctx.error);
if (ctx.computeRequest.isMockRequest) {
finishWithFailure(ctx);
return;
}
if (ctx.computeRequest.requestType != ComputeInstanceRequest.InstanceRequestType.CREATE) {
finishWithFailure(ctx);
return;
}
if (ctx.resourceGroup == null) {
finishWithFailure(ctx);
return;
}
// CREATE request has resulted in RG creation -> clear RG and its content.
String rgName = ctx.resourceGroup.name();
String msg = "Rollback provisioning for [" + ctx.vmName + "] Azure VM";
ResourceGroupsInner azureClient = getResourceManagementClientImpl(ctx)
.resourceGroups();
AzureDecommissionCallback callback = new AzureDecommissionCallback(
this, msg) {
@Override
protected DeferredResult consumeDecommissionSuccess(Void body) {
return DeferredResult.completed(body);
}
@Override
protected Throwable consumeError(Throwable e) {
String rollbackError = String.format(msg + ": FAILED. Details: %s",
Utils.toString(e));
// Wrap original ctx.error with rollback error details.
ctx.error = new IllegalStateException(rollbackError, ctx.error);
return RECOVERED;
}
@Override
protected Runnable checkExistenceCall(ServiceCallback checkExistenceCallback) {
return () -> azureClient.checkExistenceAsync(rgName, checkExistenceCallback);
}
};
azureClient.deleteAsync(rgName, callback);
callback.toDeferredResult().whenComplete((o, e) -> finishWithFailure(ctx));
}
private void finishWithFailure(AzureInstanceContext ctx) {
// Report the error back to the caller
ctx.taskManager.patchTaskToFailure(ctx.error);
ctx.close();
}
private void finishWithSuccess(AzureInstanceContext ctx) {
// Report the success back to the caller
ctx.taskManager.finishTask();
ctx.close();
}
private void createResourceGroup(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
String resourceGroupName = getResourceGroupName(ctx);
ResourceGroupInner resourceGroup = new ResourceGroupInner();
resourceGroup.withLocation(ctx.child.description.regionId);
String msg = "Creating Azure Resource Group [" + resourceGroupName + "] for [" + ctx.vmName
+ "] VM";
getResourceManagementClientImpl(ctx).resourceGroups().createOrUpdateAsync(
resourceGroupName,
resourceGroup,
new TransitionToCallback(ctx, nextStage, msg) {
@Override
CompletionStage handleSuccess(ResourceGroupInner rg) {
this.ctx.resourceGroup = rg;
return CompletableFuture.completedFuture(rg);
}
});
}
private void createStorageAccount(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
// First create SA RG (if does not exist), then create the SA itself (if does not exist)
DeferredResult.completed(ctx)
.thenCompose(this::createStorageAccountRG)
.thenCompose(this::createStorageAccount)
.whenComplete(thenAllocation(ctx, nextStage, STORAGE_NAMESPACE));
}
private DeferredResult resolveStorageAccountForPrivateImage(
AzureInstanceContext ctx) {
DiskConfiguration imageOsDisk = ctx.imageOsDisk();
if (imageOsDisk == null) {
return DeferredResult.failed(new IllegalArgumentException(
"Image OS DiskConfiguration is missing from ImageState"));
}
final String imageOsDiskUri = imageOsDisk.properties.get(AZURE_OSDISK_BLOB_URI);
if (imageOsDiskUri == null) {
return DeferredResult.failed(new IllegalArgumentException(
"OS DiskConfiguration is missing 'blob URI' property"));
}
final Matcher matcher = VHD_URI_PATTERN.matcher(imageOsDiskUri);
if (!matcher.matches()) {
return DeferredResult.failed(new IllegalArgumentException(
"Invalid VHD URI format of image OS DiskConfiguration: " + imageOsDiskUri));
}
// Override StorageAccount from request with the SA of the private VM image
ctx.storageAccountName = matcher.group(1);
String msg = "Getting Resource Group for [" + ctx.storageAccountName + "] SA for ["
+ ctx.vmName + "] VM";
AzureDeferredResultServiceCallback> handler = new AzureDeferredResultServiceCallback>(
this, msg) {
@Override
protected DeferredResult> consumeSuccess(
List result) {
// Filter by StorageAccount name
for (StorageAccountInner sA : result) {
if (Objects.equals(ctx.storageAccountName, sA.name())) {
ctx.storageAccountRGName = AzureUtils.getResourceGroupName(sA.id());
return DeferredResult.completed(result);
}
}
return DeferredResult.failed(new IllegalArgumentException(
"Unable to find SA with name: " + ctx.storageAccountName));
}
};
getStorageManagementClientImpl(ctx).storageAccounts().listAsync(handler);
return handler.toDeferredResult().thenApply(ignore -> ctx);
}
/**
* Init storage account name and resource group, using the following approach:
*
*
* AZURE_STORAGE_ACCOUNT_NAME
* AZURE_STORAGE_ACCOUNT_RG_NAME
* Used Parameter
*
*
* provided
* provided
* SA name = AZURE_STORAGE_ACCOUNT_NAME
* SA RG name = AZURE_STORAGE_ACCOUNT_RG_NAME
*
*
* provided
* not provided
* SA name = AZURE_STORAGE_ACCOUNT_NAME
* SA RG name = AZURE_STORAGE_ACCOUNT_DEFAULT_RG_NAME
*
*
* not provided
* provided
* SA name = generated name
* SA RG name = ctx.resourceGroup.getName()
*
*
* not provided
* not provided
* SA name = generated name
* SA RG name = ctx.resourceGroup.getName()
*
*
*/
private DeferredResult createStorageAccountRG(AzureInstanceContext ctx) {
if (ctx.imageSource.type == ImageSource.Type.PRIVATE_IMAGE) {
// Special handling for Private images which implies that the VM storage account
// must be same as the storage account the Custom/Private VM image resides.
return DeferredResult.completed(ctx)
.thenCompose(this::resolveStorageAccountForPrivateImage);
}
// null check in-case storage description is not set
if (ctx.bootDiskState.storageDescription != null) {
ctx.storageAccountName = ctx.bootDiskState.storageDescription.name;
} else {
ctx.storageAccountName = ctx.bootDiskState.customProperties
.get(AZURE_STORAGE_ACCOUNT_NAME);
}
ctx.storageAccountRGName = ctx.bootDiskState.customProperties
.getOrDefault(AZURE_STORAGE_ACCOUNT_RG_NAME, AZURE_STORAGE_ACCOUNT_DEFAULT_RG_NAME);
if (ctx.storageAccountName == null) {
// In case SA is not provided in the request, use request VA resource group
ctx.storageAccountName = String.valueOf(System.currentTimeMillis()) + "st";
ctx.storageAccountRGName = ctx.resourceGroup.name();
return DeferredResult.completed(ctx);
}
String msg = "Create/Update SA Resource Group [" + ctx.storageAccountRGName + "] for ["
+ ctx.vmName + "] VM";
AzureDeferredResultServiceCallback handler = new AzureDeferredResultServiceCallback(
this, msg) {
@Override
protected Throwable consumeError(Throwable exc) {
exc = super.consumeError(exc);
if (!(exc instanceof CloudException)) {
return exc;
}
final CloudError body = ((CloudException) exc).body();
if (body == null) {
return exc;
}
if (RESOURCE_GROUP_NOT_FOUND.equals(body.code())) {
return RECOVERED;
}
if (INVALID_RESOURCE_GROUP.equals(body.code())) {
String invalidParameterMsg = String.format(
"Invalid resource group parameter. %s",
body.message());
return new IllegalStateException(invalidParameterMsg, exc);
}
return exc;
}
@Override
protected DeferredResult consumeSuccess(ResourceGroupInner rg) {
return DeferredResult.completed(rg);
}
};
// Use shared RG. In case not provided in the bootDisk properties, use the default one
final ResourceGroupInner sharedSARG = new ResourceGroupInner();
sharedSARG.withLocation(ctx.child.description.regionId);
getResourceManagementClientImpl(ctx)
.resourceGroups()
.createOrUpdateAsync(ctx.storageAccountRGName, sharedSARG, handler);
return handler.toDeferredResult().thenApply(ignore -> ctx);
}
private DeferredResult createStorageAccount(AzureInstanceContext ctx) {
String msg = "Create/Update Azure Storage Account [" + ctx.storageAccountName + "] for ["
+ ctx.vmName + "] VM";
StorageAccountCreateParametersInner storageParameters = new StorageAccountCreateParametersInner();
storageParameters.withLocation(ctx.child.description.regionId);
storageParameters.withSku(new Sku().withName(SkuName.STANDARD_LRS));
storageParameters.withKind(Kind.STORAGE);
StorageAccountProvisioningCallback handler = new StorageAccountProvisioningCallback(ctx,
msg);
getStorageManagementClientImpl(ctx).storageAccounts().createAsync(
ctx.storageAccountRGName,
ctx.storageAccountName,
storageParameters,
handler);
return handler.toDeferredResult().thenApply(ignore -> ctx);
}
private void createNetworkIfNotExist(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
if (ctx.nics.isEmpty()) {
handleAllocation(ctx, nextStage);
return;
}
// Get NICs with not existing subnets
List subnetsToCreate = ctx.nics.stream()
.filter(nicCtx -> nicCtx.subnet == null)
.map(this::newAzureSubnet)
.collect(Collectors.toList());
if (subnetsToCreate.isEmpty()) {
handleAllocation(ctx, nextStage);
return;
}
createNetwork(ctx, nextStage, subnetsToCreate);
}
private void createNetwork(
AzureInstanceContext ctx,
AzureInstanceStage nextStage,
List subnetsToCreate) {
// All NICs MUST be at the same vNet (no cross vNet VMs),
// so we select the Primary vNet.
final AzureNicContext primaryNic = ctx.getPrimaryNic();
final VirtualNetworkInner vNetToCreate = newAzureVirtualNetwork(
ctx, primaryNic, subnetsToCreate);
final String vNetName = primaryNic.networkState.name;
final String vNetRGName = primaryNic.networkRGState != null
? primaryNic.networkRGState.name
: ctx.resourceGroup.name();
VirtualNetworksInner azureClient = getNetworkManagementClientImpl(ctx)
.virtualNetworks();
final String subnetNames = vNetToCreate.subnets().stream()
.map(SubnetInner::name)
.collect(Collectors.joining(","));
final String msg = "Creating Azure vNet-Subnet [v=" + vNetName + "; s="
+ subnetNames
+ "] for ["
+ ctx.vmName + "] VM";
AzureProvisioningCallback handler = new AzureProvisioningCallback(
this, msg) {
@Override
protected DeferredResult consumeProvisioningSuccess(
VirtualNetworkInner vNet) {
// Populate NICs with Azure Subnet
for (AzureNicContext nicCtx : ctx.nics) {
if (nicCtx.subnet == null) {
nicCtx.subnet = vNet.subnets().stream()
.filter(subnet -> subnet.name()
.equals(nicCtx.subnetState.name))
.findFirst().get();
}
}
return DeferredResult.completed(vNet);
}
@Override
protected String getProvisioningState(VirtualNetworkInner vNet) {
// Return first NOT Succeeded state,
// or PROVISIONING_STATE_SUCCEEDED if all are Succeeded
String subnetPS = vNet.subnets().stream()
.map(SubnetInner::provisioningState)
// Get if any is NOT Succeeded...
.filter(ps -> !PROVISIONING_STATE_SUCCEEDED.equalsIgnoreCase(ps))
// ...and return it.
.findFirst()
// Otherwise consider all are Succeeded
.orElse(PROVISIONING_STATE_SUCCEEDED);
if (PROVISIONING_STATE_SUCCEEDED.equals(vNet.provisioningState())
&& PROVISIONING_STATE_SUCCEEDED.equals(subnetPS)) {
return PROVISIONING_STATE_SUCCEEDED;
}
return vNet.provisioningState() + ":" + subnetPS;
}
@Override
protected Runnable checkProvisioningStateCall(
ServiceCallback checkProvisioningStateCallback) {
return () -> azureClient.getByResourceGroupAsync(
vNetRGName,
vNetName,
null /* expand */,
checkProvisioningStateCallback);
}
};
azureClient.createOrUpdateAsync(vNetRGName, vNetName, vNetToCreate, handler);
handler.toDeferredResult()
.thenApply(ignore -> ctx)
.whenComplete(thenAllocation(ctx, nextStage, NETWORK_NAMESPACE));
}
/**
* Converts Photon model constructs to underlying Azure VirtualNetwork model.
*/
private VirtualNetworkInner newAzureVirtualNetwork(
AzureInstanceContext ctx,
AzureNicContext nicCtx,
List subnetsToCreate) {
VirtualNetworkInner vNet = new VirtualNetworkInner();
vNet.withLocation(ctx.resourceGroup.location());
AddressSpace addressSpace = new AddressSpace()
.withAddressPrefixes(Collections.singletonList(nicCtx.networkState.subnetCIDR));
vNet.withAddressSpace(addressSpace);
vNet.withSubnets(subnetsToCreate);
return vNet;
}
/**
* Converts Photon model constructs to underlying Azure VirtualNetwork-Subnet model.
*/
private SubnetInner newAzureSubnet(AzureNicContext nicCtx) {
SubnetInner subnet = new SubnetInner();
subnet.withName(nicCtx.subnetState.name);
subnet.withAddressPrefix(nicCtx.subnetState.subnetCIDR);
return subnet;
}
private void createPublicIPs(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
if (ctx.nics.isEmpty()) {
handleAllocation(ctx, nextStage);
return;
}
AzureNicContext nicCtx = ctx.getPrimaryNic();
// For now if not specified default to TRUE!
if (nicCtx.nicStateWithDesc.description.assignPublicIpAddress == null) {
nicCtx.nicStateWithDesc.description.assignPublicIpAddress = Boolean.TRUE;
}
if (nicCtx.nicStateWithDesc.description.assignPublicIpAddress == Boolean.FALSE) {
// Do nothing in this method -> proceed to next stage.
handleAllocation(ctx, nextStage);
return;
}
PublicIPAddressesInner azureClient = getNetworkManagementClientImpl(ctx)
.publicIPAddresses();
final PublicIPAddressInner publicIPAddress = newAzurePublicIPAddress(ctx, nicCtx);
final String publicIPName = ctx.vmName + "-pip";
final String publicIPRGName = ctx.resourceGroup.name();
String msg = "Creating Azure Public IP [" + publicIPName + "] for [" + ctx.vmName + "] VM";
AzureProvisioningCallback handler = new AzureProvisioningCallback(
this, msg) {
@Override
protected DeferredResult consumeProvisioningSuccess(
PublicIPAddressInner publicIP) {
nicCtx.publicIP = publicIP;
return DeferredResult.completed(publicIP);
}
@Override
protected String getProvisioningState(PublicIPAddressInner publicIP) {
return publicIP.provisioningState();
}
@Override
protected Runnable checkProvisioningStateCall(
ServiceCallback checkProvisioningStateCallback) {
return () -> azureClient.getByResourceGroupAsync(
publicIPRGName,
publicIPName,
null /* expand */,
checkProvisioningStateCallback);
}
};
azureClient.createOrUpdateAsync(publicIPRGName, publicIPName, publicIPAddress, handler);
handler.toDeferredResult()
.thenApply(ignore -> ctx)
.whenComplete(thenAllocation(ctx, nextStage, NETWORK_NAMESPACE));
}
/**
* Converts Photon model constructs to underlying Azure PublicIPAddress model.
*/
private PublicIPAddressInner newAzurePublicIPAddress(
AzureInstanceContext ctx,
AzureNicContext nicCtx) {
PublicIPAddressInner publicIPAddress = new PublicIPAddressInner();
publicIPAddress.withLocation(ctx.resourceGroup.location());
publicIPAddress
.withPublicIPAllocationMethod(new IPAllocationMethod(
nicCtx.nicStateWithDesc.description.assignment.name()));
return publicIPAddress;
}
private void createSecurityGroupsIfNotExist(AzureInstanceContext ctx,
AzureInstanceStage nextStage) {
if (ctx.nics.isEmpty()) {
handleAllocation(ctx, nextStage);
return;
}
NetworkSecurityGroupsInner azureClient = getNetworkManagementClientImpl(ctx)
.networkSecurityGroups();
List> createSGDR = ctx.nics.stream()
// Security Group is requested but no existing security group is mapped.
.filter(nicCtx -> nicCtx.securityGroupState() != null
&& nicCtx.securityGroup == null)
.map(nicCtx -> {
SecurityGroupState sgState = nicCtx.securityGroupState();
String rgName = nicCtx.securityGroupRGState != null
? nicCtx.securityGroupRGState.name
: ctx.resourceGroup.name();
String msg = "Create Azure Security Group ["
+ rgName + "/" + sgState.name
+ "] for [" + nicCtx.nicStateWithDesc.name + "] NIC for ["
+ ctx.vmName
+ "] VM";
return AzureSecurityGroupUtils.createSecurityGroup(this, azureClient,
sgState, rgName, ctx.resourceGroup.location(), msg)
.thenApply(sg -> {
nicCtx.securityGroup = sg;
return sg;
});
})
.collect(Collectors.toList());
DeferredResult.allOf(createSGDR).whenComplete((all, exc) -> {
if (exc != null) {
handleError(ctx, exc);
} else {
handleAllocation(ctx, nextStage);
}
});
}
private void createNICs(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
if (ctx.nics.isEmpty()) {
handleAllocation(ctx, nextStage);
return;
}
// Shared state between multi async calls {{
AzureCallContext callContext = AzureCallContext.newBatchCallContext(ctx.nics.size());
NetworkInterfacesInner azureClient = getNetworkManagementClientImpl(ctx)
.networkInterfaces();
// }}
for (AzureNicContext nicCtx : ctx.nics) {
final String nicName = nicCtx.nicStateWithDesc.name;
final NetworkInterfaceInner nic = newAzureNetworkInterface(ctx, nicCtx);
String msg = "Creating Azure NIC [" + nicName + "] for [" + ctx.vmName + "] VM";
azureClient.createOrUpdateAsync(
ctx.resourceGroup.name(),
nicName,
nic,
new TransitionToCallback(ctx, nextStage, callContext,
msg) {
@Override
protected CompletionStage handleSuccess(
NetworkInterfaceInner nic) {
nicCtx.nic = nic;
return CompletableFuture.completedFuture(nic);
}
});
}
}
private void generateVmId(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
ComputeState cs = new ComputeState();
// Set id to the compute state so any new triggered enumeration can patch it instead of
// creating a new ComputeState, effectively duplicating the ComputeStates related to the
// same VM resource.
cs.id = AzureUtils.getVirtualMachineId(
ctx.parentAuth.userLink,
ctx.resourceGroup.name(),
ctx.vmName);
sendWithDeferredResult(
Operation.createPatch(ctx.computeRequest.resourceReference)
.setBody(cs))
.thenApply(op -> ctx)
.whenComplete(thenAllocation(ctx, nextStage));
}
/**
* Converts Photon model constructs to underlying Azure NetworkInterface model.
*/
private NetworkInterfaceInner newAzureNetworkInterface(
AzureInstanceContext ctx,
AzureNicContext nicCtx) {
NetworkInterfaceDescription description = nicCtx.nicStateWithDesc.description;
NetworkInterfaceIPConfigurationInner ipConfig = new NetworkInterfaceIPConfigurationInner();
ipConfig.withName(generateName(NICCONFIG_NAME_PREFIX));
ipConfig.withSubnet(nicCtx.subnet);
if (nicCtx.publicIP != null) {
// Public IP is not auto-assigned so check for existence
ipConfig.withPublicIPAddress(new SubResource().withId(nicCtx.publicIP.id()));
}
ipConfig.withPrivateIPAllocationMethod(
new IPAllocationMethod(description.assignment.name()));
if (description.assignment == IpAssignment.STATIC) {
ipConfig.withPrivateIPAddress(description.address);
}
NetworkInterfaceInner nic = new NetworkInterfaceInner();
nic.withLocation(ctx.resourceGroup.location());
nic.withIpConfigurations(Collections.singletonList(ipConfig));
if (nicCtx.securityGroup != null) {
// Security group is optional so check for existence
nic.withNetworkSecurityGroup(new SubResource().withId(nicCtx.securityGroup.id()));
}
return nic;
}
private void createVM(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
ComputeDescriptionService.ComputeDescription description = ctx.child.description;
Map customProperties = description.customProperties;
if (customProperties == null) {
handleError(ctx, new IllegalStateException("Custom properties not specified"));
return;
}
DiskState bootDisk = ctx.bootDiskState;
if (bootDisk == null) {
handleError(ctx, new IllegalStateException("Azure bootDisk not specified"));
return;
}
String cloudConfig = null;
if (bootDisk.bootConfig != null
&& bootDisk.bootConfig.files.length > CLOUD_CONFIG_DEFAULT_FILE_INDEX) {
cloudConfig = bootDisk.bootConfig.files[CLOUD_CONFIG_DEFAULT_FILE_INDEX].contents;
}
VirtualMachineInner request = new VirtualMachineInner();
request.withLocation(ctx.resourceGroup.location());
// Set OS profile.
OSProfile osProfile = new OSProfile();
osProfile.withComputerName(ctx.vmName);
if (ctx.childAuth != null) {
osProfile.withAdminUsername(ctx.childAuth.userEmail);
osProfile.withAdminPassword(EncryptionUtils.decrypt(ctx.childAuth.privateKey));
}
if (cloudConfig != null) {
try {
osProfile.withCustomData(Base64.getEncoder()
.encodeToString(cloudConfig.getBytes(Utils.CHARSET)));
} catch (UnsupportedEncodingException e) {
logWarning(() -> "Error encoding user data");
return;
}
}
request.withOsProfile(osProfile);
// Set hardware profile.
HardwareProfile hardwareProfile = new HardwareProfile();
hardwareProfile.withVmSize(
description.instanceType != null
? new VirtualMachineSizeTypes(description.instanceType)
: VirtualMachineSizeTypes.BASIC_A0);
request.withHardwareProfile(hardwareProfile);
// Set storage profile.
// Create destination OS VHD
final OSDisk osDisk = newAzureOsDisk(ctx);
final StorageProfile storageProfile = new StorageProfile();
// Apply Public/Private images specifics
if (ctx.imageSource.type == ImageSource.Type.PUBLIC_IMAGE
|| ctx.imageSource.type == ImageSource.Type.IMAGE_REFERENCE) {
storageProfile.withImageReference(ctx.imageReference);
} else if (ctx.imageSource.type == ImageSource.Type.PRIVATE_IMAGE) {
final ImageState privateImage = ctx.imageSource.asImageState();
// In case of PRIVATE images do EXTRA OSDisk configuration
// Image OS type
osDisk.withOsType(OperatingSystemTypes.fromString(privateImage.osFamily));
// Ref to OS disk (VHD) of the image
osDisk.withImage(new VirtualHardDisk().withUri(
ctx.imageOsDisk().properties.get(AZURE_OSDISK_BLOB_URI)));
}
storageProfile.withOsDisk(osDisk);
storageProfile.withDataDisks(newAzureDataDisks(ctx));
request.withStorageProfile(storageProfile);
// Set network profile {{
NetworkProfile networkProfile = new NetworkProfile();
networkProfile.withNetworkInterfaces(new ArrayList<>());
for (AzureNicContext nicCtx : ctx.nics) {
NetworkInterfaceReferenceInner nicRef = new NetworkInterfaceReferenceInner();
nicRef.withId(nicCtx.nic.id());
// NOTE: First NIC is marked as Primary.
nicRef.withPrimary(networkProfile.networkInterfaces().isEmpty());
networkProfile.networkInterfaces().add(nicRef);
}
request.withNetworkProfile(networkProfile);
logFine(() -> String.format("Creating virtual machine with name [%s]", ctx.vmName));
getComputeManagementClientImpl(ctx).virtualMachines().createOrUpdateAsync(
ctx.resourceGroup.name(), ctx.vmName, request,
new AzureAsyncCallback() {
@Override
public void onError(Throwable e) {
handleCloudError(
String.format("Provisioning VM %s: FAILED. Details:", ctx.vmName),
ctx,
COMPUTE_NAMESPACE, e);
}
@Override
public void onSuccess(VirtualMachineInner result) {
logFine(() -> String.format("Successfully created vm [%s]", result.name()));
ctx.provisionedVm = result;
ComputeState cs = new ComputeState();
// Azure for some case changes the case of the vm id.
ctx.vmId = result.id().toLowerCase();
cs.id = ctx.vmId;
cs.type = ComputeType.VM_GUEST;
cs.environmentName = ComputeDescription.ENVIRONMENT_NAME_AZURE;
cs.lifecycleState = LifecycleState.READY;
if (ctx.child.customProperties == null) {
cs.customProperties = new HashMap<>();
} else {
cs.customProperties = ctx.child.customProperties;
}
cs.customProperties.put(RESOURCE_GROUP_NAME, ctx.resourceGroup.name());
Operation.CompletionHandler completionHandler = (ox, exc) -> {
if (exc != null) {
handleError(ctx, exc);
return;
}
handleAllocation(ctx, nextStage);
};
sendRequest(
Operation.createPatch(ctx.computeRequest.resourceReference)
.setBody(cs)
.setCompletion(completionHandler));
}
});
}
/**
* Converts Photon model boot DiskState to underlying Azure OSDisk model.
*/
private OSDisk newAzureOsDisk(AzureInstanceContext ctx) {
final OSDisk azureOsDisk = new OSDisk();
azureOsDisk.withName(ctx.bootDiskState.name);
azureOsDisk.withVhd(getVHDUriForOSDisk(ctx.vmName, ctx.storageAccountName));
// We don't support Attach option which allows to use a specialized disk to create the
// virtual machine.
azureOsDisk.withCreateOption(DiskCreateOptionTypes.FROM_IMAGE);
if (ctx.bootDiskState.customProperties != null &&
ctx.bootDiskState.customProperties.get(AZURE_OSDISK_CACHING) != null) {
azureOsDisk.withCaching(CachingTypes.fromString(
ctx.bootDiskState.customProperties.get(AZURE_OSDISK_CACHING)));
} else {
// Recommended default caching for OS disk
azureOsDisk.withCaching(CachingTypes.NONE);
}
if (ctx.bootDiskState.capacityMBytes > 31744
&& ctx.bootDiskState.capacityMBytes < AZURE_MAXIMUM_OS_DISK_SIZE_MB) {
// In case custom boot disk size is set then use that
// value. If value more than maximum allowed then proceed with default size.
// Converting MBs to GBs and casting as int
int diskSizeInGB = (int) ctx.bootDiskState.capacityMBytes / 1024;
azureOsDisk.withDiskSizeGB(diskSizeInGB);
} else {
logInfo(() -> String.format(
"Proceeding with Default OS Disk Size defined by VHD %s",
azureOsDisk.vhd().uri()));
}
return azureOsDisk;
}
/**
* Converts Photon model data DiskState to underlying Azure DataDisk model.
*/
private List newAzureDataDisks(AzureInstanceContext ctx) {
int lunIndex = 0;
final List azureDataDisks = new ArrayList<>();
for (DiskState diskState : ctx.dataDiskStates) {
final DataDisk dataDisk = new DataDisk();
dataDisk.withName(diskState.name);
dataDisk.withVhd(getVHDUriForDataDisk(ctx.vmName, ctx.storageAccountName, lunIndex));
dataDisk.withCreateOption(DiskCreateOptionTypes.EMPTY);
dataDisk.withDiskSizeGB((int) diskState.capacityMBytes / 1024);
dataDisk.withCaching(CachingTypes.fromString(
diskState.customProperties.getOrDefault(
AZURE_DATA_DISK_CACHING,
CachingTypes.READ_WRITE.toString())));
dataDisk.withLun(lunIndex);
azureDataDisks.add(dataDisk);
lunIndex++;
}
return azureDataDisks;
}
/**
* Update Compute related local state details with the actual data from Azure. This includes:
*
* - setting the public address to the ComputeState
* - setting private IP addresses to NetworkInterfaceState objects
* - setting VHD URI to DiskState objects
*
*/
private void updateComputeStateDetails(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
DeferredResult.completed(ctx)
.thenCompose(this::getPublicIPAddress)
.thenCompose(this::updateComputeState)
.thenCompose(this::updateNicStates)
.thenCompose(this::updateDiskStates)
.whenComplete(thenAllocation(ctx, nextStage));
}
private DeferredResult getPublicIPAddress(AzureInstanceContext ctx) {
if (ctx.getPrimaryNic().publicIP == null) {
// No public IP address created -> do nothing.
return DeferredResult.completed(ctx);
}
NetworkManagementClientImpl client = getNetworkManagementClientImpl(ctx);
String msg = "Get public IP address for resource group [" + ctx.resourceGroup.name()
+ "] and name [" + ctx.getPrimaryNic().publicIP.name() + "].";
AzureDeferredResultServiceCallback callback = new AzureDeferredResultServiceCallback(
ctx.service, msg) {
@Override
protected DeferredResult consumeSuccess(
PublicIPAddressInner result) {
ctx.getPrimaryNic().publicIP = result;
return DeferredResult.completed(result);
}
};
client.publicIPAddresses().getByResourceGroupAsync(
ctx.resourceGroup.name(),
ctx.getPrimaryNic().publicIP.name(),
null /* expand */,
callback);
return callback.toDeferredResult().thenApply(ignored -> ctx);
}
/**
* Update {@code computeState.address} with Public IP, if presented.
*/
private DeferredResult updateComputeState(AzureInstanceContext ctx) {
if (ctx.getPrimaryNic().publicIP == null) {
// Do nothing.
return DeferredResult.completed(ctx);
}
ComputeState computeState = new ComputeState();
computeState.address = ctx.getPrimaryNic().publicIP.ipAddress();
Operation updateCS = Operation.createPatch(ctx.computeRequest.resourceReference)
.setBody(computeState);
return ctx.service
.sendWithDeferredResult(updateCS)
.thenApply(ignore -> {
logFine(() -> String.format(
"Updating Compute state [%s] with Public IP [%s]: SUCCESS",
ctx.vmName, computeState.address));
return ctx;
});
}
/**
* Update {@code computeState.nicState[i].address} with Azure NICs' private IP.
*/
private DeferredResult updateNicStates(AzureInstanceContext ctx) {
if (ctx.nics == null || ctx.nics.isEmpty()) {
// Do nothing.
return DeferredResult.completed(ctx);
}
List> updateNICsDR = new ArrayList<>(ctx.nics.size());
for (AzureNicContext nicCtx : ctx.nics) {
if (nicCtx.nic == null) {
continue;
}
final NetworkInterfaceState nicStateToUpdate = new NetworkInterfaceState();
nicStateToUpdate.id = nicCtx.nic.id();
nicStateToUpdate.documentSelfLink = nicCtx.nicStateWithDesc.documentSelfLink;
if (nicCtx.nic.ipConfigurations() != null && !nicCtx.nic.ipConfigurations().isEmpty()) {
nicStateToUpdate.address = nicCtx.nic.ipConfigurations().get(0).privateIPAddress();
}
Operation updateNicOp = Operation
.createPatch(ctx.service, nicStateToUpdate.documentSelfLink)
.setBody(nicStateToUpdate);
DeferredResult updateNicDR = ctx.service.sendWithDeferredResult(updateNicOp)
.thenAccept(ignored -> logFine(() -> String.format(
"Updating NIC state [%s] with Private IP [%s]: SUCCESS",
nicCtx.nic.name(), nicStateToUpdate.address)));
updateNICsDR.add(updateNicDR);
}
return DeferredResult.allOf(updateNICsDR).thenApply(ignored -> ctx);
}
/**
* Update {@code computeState.diskState[i].id} with Azure Disks' VHD URI.
*/
private DeferredResult updateDiskStates(AzureInstanceContext ctx) {
if (ctx.provisionedVm == null) {
// Do nothing.
return DeferredResult.completed(ctx);
}
List> updateDiskStateDRs = new ArrayList<>();
// Update boot DiskState with Azure osDisk VHD URI
{
final OSDisk azureOsDisk = ctx.provisionedVm.storageProfile().osDisk();
final DiskState diskStateToUpdate = new DiskState();
diskStateToUpdate.documentSelfLink = ctx.bootDiskState.documentSelfLink;
// The actual value being updated
diskStateToUpdate.id = azureOsDisk.vhd().uri();
Operation updateDiskState = Operation
.createPatch(ctx.service, diskStateToUpdate.documentSelfLink)
.setBody(diskStateToUpdate);
DeferredResult updateDR = ctx.service.sendWithDeferredResult(updateDiskState)
.whenComplete((op, exc) -> {
if (exc != null) {
logSevere(() -> String.format(
"Updating boot DiskState [%s] with VHD URI [%s]: FAILED with %s",
ctx.bootDiskState.name, diskStateToUpdate.id,
Utils.toString(exc)));
} else {
logFine(() -> String.format(
"Updating boot DiskState [%s] with VHD URI [%s]: SUCCESS",
ctx.bootDiskState.name, diskStateToUpdate.id));
}
});
updateDiskStateDRs.add(updateDR);
}
for (DataDisk azureDataDisk : ctx.provisionedVm.storageProfile().dataDisks()) {
// Find corresponding DiskState by name
DiskState dataDiskState = ctx.dataDiskStates.stream()
.filter(dS -> azureDataDisk.name().equals(dS.name))
.findFirst()
.get();
final DiskState diskStateToUpdate = new DiskState();
diskStateToUpdate.documentSelfLink = dataDiskState.documentSelfLink;
// The actual value being updated
diskStateToUpdate.id = azureDataDisk.vhd().uri();
Operation updateDiskState = Operation
.createPatch(ctx.service, diskStateToUpdate.documentSelfLink)
.setBody(diskStateToUpdate);
DeferredResult updateDR = ctx.service.sendWithDeferredResult(updateDiskState)
.whenComplete((op, exc) -> {
if (exc != null) {
logSevere(() -> String.format(
"Updating data DiskState [%s] with VHD URI [%s]: FAILED with %s",
dataDiskState.name, diskStateToUpdate.id, Utils.toString(exc)));
} else {
logFine(() -> String.format(
"Updating data DiskState [%s] with VHD URI [%s]: SUCCESS",
dataDiskState.name, diskStateToUpdate.id));
}
});
updateDiskStateDRs.add(updateDR);
}
return DeferredResult.allOf(updateDiskStateDRs).thenApply(ignored -> ctx);
}
/**
* Gets the storage keys from azure and patches the credential state.
*/
private void getStorageKeys(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
StorageManagementClientImpl client = getStorageManagementClientImpl(ctx);
client.storageAccounts().listKeysAsync(ctx.storageAccountRGName,
ctx.storageAccountName,
new AzureAsyncCallback() {
@Override
public void onError(Throwable e) {
handleError(ctx, e);
}
@Override
public void onSuccess(StorageAccountListKeysResultInner result) {
logFine(() -> String.format("Retrieved the storage account keys for storage"
+ " account [%s]", ctx.storageAccountName));
AuthCredentialsServiceState storageAuth = new AuthCredentialsServiceState();
storageAuth.customProperties = new HashMap<>();
for (StorageAccountKey key : result.keys()) {
storageAuth.customProperties.put(
getStorageAccountKeyName(storageAuth.customProperties),
key.value());
}
Operation patchStorageDescriptionWithKeys = Operation
.createPost(getHost(), AuthCredentialsService.FACTORY_LINK)
.setBody(storageAuth).setCompletion((o, e) -> {
if (e != null) {
handleError(ctx, e);
return;
}
AuthCredentialsServiceState resultAuth = o.getBody(
AuthCredentialsServiceState.class);
ctx.storageDescription.authCredentialsLink = resultAuth.documentSelfLink;
Operation patch = Operation
.createPatch(UriUtils.buildUri(getHost(),
ctx.storageDescription.documentSelfLink))
.setBody(ctx.storageDescription)
.setCompletion(((completedOp, failure) -> {
if (failure != null) {
handleError(ctx, failure);
return;
}
logFine(() -> "Patched storage description.");
handleAllocation(ctx, nextStage);
}));
sendRequest(patch);
});
sendRequest(patchStorageDescriptionWithKeys);
}
});
}
private static VirtualHardDisk getVHDUriForOSDisk(String vmName, String storageAccountName) {
String vhdName = vmName + BOOT_DISK_SUFFIX;
return new VirtualHardDisk().withUri(
String.format(VHD_URI_FORMAT, storageAccountName, vhdName));
}
private static VirtualHardDisk getVHDUriForDataDisk(String vmName, String storageAccountName,
int num) {
String vhdName = vmName + DATA_DISK_SUFFIX + "-" + num;
return new VirtualHardDisk().withUri(
String.format(VHD_URI_FORMAT, storageAccountName, vhdName));
}
private ImageReferenceInner getImageReference(String imageId) {
String[] imageIdParts = imageId.split(":");
if (imageIdParts.length != 4) {
throw new IllegalArgumentException("Azure image ID should be of the format "
+ ":::");
}
ImageReferenceInner imageReference = new ImageReferenceInner();
imageReference.withPublisher(imageIdParts[0]);
imageReference.withOffer(imageIdParts[1]);
imageReference.withSku(imageIdParts[2]);
imageReference.withVersion(imageIdParts[3]);
return imageReference;
}
/**
* This method tries to detect specific CloudErrors by their code and apply some additional
* handling. In case a subscription registration error is detected the method register
* subscription for given namespace. In case invalid parameter error is detected, the error
* message is made better human-readable. Otherwise the fallback is to transition to error state
* through next specific error handler.
*/
private void handleCloudError(String msg, AzureInstanceContext ctx, String namespace,
Throwable e) {
if (e instanceof CloudException) {
CloudException ce = (CloudException) e;
CloudError body = ce.body();
if (body != null) {
String code = body.code();
if (MISSING_SUBSCRIPTION_CODE.equals(code)) {
registerSubscription(ctx, namespace);
return;
} else if (INVALID_PARAMETER.equals(code) || INVALID_RESOURCE_GROUP.equals(code)) {
String invalidParameterMsg = String.format("%s Invalid parameter. %s",
msg, body.message());
e = new IllegalStateException(invalidParameterMsg, ctx.error);
handleError(ctx, e);
return;
}
}
}
handleError(ctx, e);
}
private void registerSubscription(AzureInstanceContext ctx, String namespace) {
ResourceManagementClientImpl client = getResourceManagementClientImpl(ctx);
client.providers().registerAsync(namespace,
new AzureAsyncCallback() {
@Override
public void onError(Throwable e) {
handleError(ctx, e);
}
@Override
public void onSuccess(ProviderInner result) {
String registrationState = result.registrationState();
if (!PROVIDER_REGISTRED_STATE.equalsIgnoreCase(registrationState)) {
logInfo(() -> String.format("%s namespace registration in %s state",
namespace, registrationState));
long retryExpiration = Utils.getNowMicrosUtc()
+ DEFAULT_EXPIRATION_INTERVAL_MICROS;
getSubscriptionState(ctx, namespace, retryExpiration);
return;
}
logFine(() -> String.format("Successfully registered namespace [%s]",
result.namespace()));
handleAllocation(ctx);
}
});
}
private void getSubscriptionState(AzureInstanceContext ctx,
String namespace, long retryExpiration) {
if (Utils.getNowMicrosUtc() > retryExpiration) {
String msg = String.format("Subscription for %s namespace did not reach %s state",
namespace, PROVIDER_REGISTRED_STATE);
handleError(ctx, new RuntimeException(msg));
return;
}
ResourceManagementClientImpl client = getResourceManagementClientImpl(ctx);
getHost().schedule(
() -> client.providers().getAsync(namespace,
new AzureAsyncCallback() {
@Override
public void onError(Throwable e) {
handleError(ctx, e);
}
@Override
public void onSuccess(ProviderInner result) {
String registrationState = result.registrationState();
if (!PROVIDER_REGISTRED_STATE.equalsIgnoreCase(registrationState)) {
logInfo(() -> String.format(
"%s namespace registration in %s state",
namespace, registrationState));
getSubscriptionState(ctx, namespace, retryExpiration);
return;
}
logFine(() -> String.format(
"Successfully registered namespace [%s]",
result.namespace()));
handleAllocation(ctx);
}
}),
RETRY_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
private void getChildAuth(AzureInstanceContext ctx, AzureInstanceStage next) {
if (ctx.child.description.authCredentialsLink == null) {
AuthCredentialsServiceState auth = new AuthCredentialsServiceState();
auth.userEmail = AzureConstants.DEFAULT_ADMIN_USER;
auth.privateKey = AzureConstants.DEFAULT_ADMIN_PASSWORD;
ctx.childAuth = auth;
handleAllocation(ctx, next);
return;
}
String childAuthLink = ctx.child.description.authCredentialsLink;
Consumer onSuccess = (op) -> {
ctx.childAuth = op.getBody(AuthCredentialsService.AuthCredentialsServiceState.class);
handleAllocation(ctx, next);
};
AdapterUtils.getServiceState(this, childAuthLink, onSuccess, getFailureConsumer(ctx));
}
private Consumer getFailureConsumer(AzureInstanceContext ctx) {
return (t) -> handleError(ctx, t);
}
private String generateName(String prefix) {
return prefix + randomString(5);
}
private String randomString(int length) {
Random random = new Random();
StringBuilder stringBuilder = new StringBuilder(length);
for (int i = 0; i < length; i++) {
stringBuilder.append((char) ('a' + random.nextInt(26)));
}
return stringBuilder.toString();
}
private ResourceManagementClientImpl getResourceManagementClientImpl(AzureInstanceContext ctx) {
return ctx.azureSdkClients.getResourceManagementClientImpl();
}
public NetworkManagementClientImpl getNetworkManagementClientImpl(AzureInstanceContext ctx) {
return ctx.azureSdkClients.getNetworkManagementClientImpl();
}
private StorageManagementClientImpl getStorageManagementClientImpl(AzureInstanceContext ctx) {
return ctx.azureSdkClients.getStorageManagementClientImpl();
}
private ComputeManagementClientImpl getComputeManagementClientImpl(AzureInstanceContext ctx) {
return ctx.azureSdkClients.getComputeManagementClientImpl();
}
/**
* Method will retrieve disks for targeted image
*/
private void getVMDisks(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
if (ctx.child.diskLinks == null || ctx.child.diskLinks.size() == 0) {
handleError(ctx, new IllegalStateException("a minimum of 1 disk is required"));
return;
}
Collection operations = new ArrayList<>();
// iterate thru disks and create operations
operations.addAll(ctx.child.diskLinks.stream()
.map(disk -> Operation.createGet(UriUtils.buildExpandLinksQueryUri(
UriUtils.buildUri(this.getHost(), disk))))
.collect(Collectors.toList()));
OperationJoin operationJoin = OperationJoin.create(operations)
.setCompletion(
(ops, exc) -> {
if (exc != null) {
handleError(ctx, new IllegalStateException(
"Error getting disk information"));
return;
}
ctx.dataDiskStates = new ArrayList<>();
for (Operation op : ops.values()) {
DiskService.DiskStateExpanded disk = op
.getBody(DiskService.DiskStateExpanded.class);
// We treat the first disk in the boot order as the boot disk.
if (disk.bootOrder == 1) {
if (ctx.bootDiskState != null) {
handleError(ctx, new IllegalStateException(
"Only 1 boot disk is allowed"));
return;
}
ctx.bootDiskState = disk;
} else {
ctx.dataDiskStates.add(disk);
}
}
if (ctx.bootDiskState == null) {
handleError(ctx,
new IllegalStateException("Boot disk is required"));
return;
}
handleAllocation(ctx, nextStage);
});
operationJoin.sendWith(this);
}
/**
* Differentiate between Windows and Linux Images
*/
private DeferredResult getImageSource(AzureInstanceContext ctx) {
return ctx.getImageSource(ctx.bootDiskState)
.thenApply(imageSource -> {
ctx.imageSource = imageSource;
return ctx;
})
.thenCompose(context -> {
if (context.imageSource.type == ImageSource.Type.PUBLIC_IMAGE) {
return handlePublicImage(context);
}
if (context.imageSource.type == ImageSource.Type.PRIVATE_IMAGE) {
return handlePrivateImage(context);
}
if (context.imageSource.type == ImageSource.Type.IMAGE_REFERENCE) {
return handleImageRef(context);
}
return DeferredResult.failed(
new IllegalStateException(
"Unexpected ImageSource.Type: " + context.imageSource.type));
});
}
private DeferredResult handlePublicImage(AzureInstanceContext ctx) {
return DeferredResult.completed(ctx)
.thenApply(context -> {
// Convert Azure 'ImageState.id' string to ImageReferenceInner object
context.imageReference = getImageReference(context.imageSource.asNativeId());
return context;
})
.thenCompose(this::resolveLatestVirtualMachineImage);
}
private DeferredResult handlePrivateImage(AzureInstanceContext ctx) {
return DeferredResult.completed(ctx);
}
private DeferredResult handleImageRef(AzureInstanceContext ctx) {
return DeferredResult.completed(ctx)
.thenApply(context -> {
// Convert Azure 'source image reference' string to ImageReferenceInner object
context.imageReference = getImageReference(context.imageSource.asNativeId());
return context;
})
.thenCompose(this::resolveLatestVirtualMachineImage);
}
/**
* Get the LATEST VirtualMachineImage using publisher, offer and SKU.
*/
private DeferredResult resolveLatestVirtualMachineImage(
AzureInstanceContext ctx) {
if (AzureConstants.AZURE_URN_VERSION_LATEST
.equalsIgnoreCase(ctx.imageReference.version())) {
String msg = String.format("Getting latest Azure image by %s:%s:%s",
ctx.imageReference.publisher(),
ctx.imageReference.offer(),
ctx.imageReference.sku());
AzureDeferredResultServiceCallback> callback = new AzureDeferredResultServiceCallback>(
ctx.service, msg) {
@Override
protected DeferredResult> consumeSuccess(
List imageResources) {
return DeferredResult.completed(imageResources);
}
};
getComputeManagementClientImpl(ctx).virtualMachineImages().listAsync(
ctx.resourceGroup.location(),
ctx.imageReference.publisher(),
ctx.imageReference.offer(),
ctx.imageReference.sku(),
null,
1,
AzureConstants.ORDER_BY_VM_IMAGE_RESOURCE_NAME_DESC,
callback);
return callback.toDeferredResult().thenCompose(imageResources -> {
if (imageResources == null
|| imageResources.isEmpty()
|| imageResources.get(0) == null) {
return DeferredResult
.failed(new IllegalStateException("No latest version found"));
}
// Update 'latest'-version with actual version
ctx.imageReference.withVersion(imageResources.get(0).name());
return DeferredResult.completed(ctx);
});
}
return DeferredResult.completed(ctx);
}
private void enableMonitoring(AzureInstanceContext ctx, AzureInstanceStage nextStage) {
Operation readFile = Operation.createGet(null).setCompletion((o, e) -> {
if (e != null) {
handleError(ctx, e);
return;
}
AzureDiagnosticSettings azureDiagnosticSettings = o
.getBody(AzureDiagnosticSettings.class);
String vmName = ctx.vmName;
String azureInstanceId = ctx.vmId;
String storageAccountName = ctx.storageAccountName;
// Replace the resourceId and storageAccount keys with correct values
azureDiagnosticSettings.getProperties()
.getPublicConfiguration()
.getDiagnosticMonitorConfiguration()
.getMetrics()
.setResourceId(azureInstanceId);
azureDiagnosticSettings.getProperties()
.getPublicConfiguration()
.setStorageAccount(storageAccountName);
ApplicationTokenCredentials credentials = ctx.azureSdkClients.credentials;
URI uri = UriUtils.extendUriWithQuery(
UriUtils.buildUri(UriUtils.buildUri(AzureConstants.BASE_URI_FOR_REST),
azureInstanceId, AzureConstants.DIAGNOSTIC_SETTING_ENDPOINT,
AzureConstants.DIAGNOSTIC_SETTING_AGENT),
AzureConstants.QUERY_PARAM_API_VERSION,
AzureConstants.DIAGNOSTIC_SETTING_API_VERSION);
Operation operation = Operation.createPut(uri);
operation.setBody(azureDiagnosticSettings);
operation.addRequestHeader(Operation.ACCEPT_HEADER,
Operation.MEDIA_TYPE_APPLICATION_JSON);
operation.addRequestHeader(Operation.CONTENT_TYPE_HEADER,
Operation.MEDIA_TYPE_APPLICATION_JSON);
try {
operation.addRequestHeader(Operation.AUTHORIZATION_HEADER,
AzureConstants.AUTH_HEADER_BEARER_PREFIX
+ credentials.getToken(AZURE_CORE_MANAGEMENT_URI));
} catch (Exception ex) {
handleError(ctx, ex);
return;
}
logFine(() -> String.format("Enabling monitoring on the VM [%s]", vmName));
operation.setCompletion((op, er) -> {
if (er != null) {
handleError(ctx, er);
return;
}
logFine(() -> String.format("Successfully enabled monitoring on the VM [%s]",
vmName));
handleAllocation(ctx, nextStage);
});
sendRequest(operation);
});
String fileUri = getClass().getResource(AzureConstants.DIAGNOSTIC_SETTINGS_JSON_FILE_NAME)
.getFile();
File jsonPayloadFile = new File(fileUri);
try {
FileUtils.readFileAndComplete(readFile, jsonPayloadFile);
} catch (Exception e) {
handleError(ctx, e);
}
}
private String getResourceGroupName(AzureInstanceContext ctx) {
String resourceGroupName = null;
if (ctx.child.customProperties != null) {
resourceGroupName = ctx.child.customProperties.get(RESOURCE_GROUP_NAME);
}
if (resourceGroupName == null && ctx.child.description.customProperties != null) {
resourceGroupName = ctx.child.description.customProperties.get(RESOURCE_GROUP_NAME);
}
if (resourceGroupName == null || resourceGroupName.isEmpty()) {
resourceGroupName = DEFAULT_GROUP_PREFIX + String.valueOf(System.currentTimeMillis());
}
return resourceGroupName;
}
private class StorageAccountProvisioningCallback
extends AzureProvisioningCallback {
private final AzureInstanceContext ctx;
StorageAccountProvisioningCallback(AzureInstanceContext ctx, String message) {
super(ctx.service, message);
this.ctx = ctx;
}
@Override
protected Throwable consumeError(Throwable exc) {
exc = super.consumeError(exc);
if (exc instanceof CloudException) {
CloudException azureExc = (CloudException) exc;
if (STORAGE_ACCOUNT_ALREADY_EXIST
.equalsIgnoreCase(azureExc.body().code())) {
return RECOVERED;
}
}
return exc;
}
@Override
protected DeferredResult consumeProvisioningSuccess(
StorageAccountInner sa) {
this.ctx.storageAccount = sa;
return createStorageDescription(this.ctx)
// Start next op, patch boot disk, in the sequence
.thenCompose(woid -> {
URI uri = this.ctx.computeRequest.buildUri(
this.ctx.bootDiskState.documentSelfLink);
Operation patchBootDiskOp = Operation
.createPatch(uri)
.setBody(this.ctx.bootDiskState);
return this.ctx.service.sendWithDeferredResult(patchBootDiskOp)
.thenRun(() -> this.ctx.service.logFine(
() -> String.format("Updating boot disk [%s]: SUCCESS",
this.ctx.bootDiskState.name)));
})
// Return processed context with StorageAccount
.thenApply(woid -> this.ctx.storageAccount);
}
/**
* Based on the queried result, in case no SA description exists for the given name, create
* a new one. For this purpose, StorageAccountKeys should be obtained, and with them
* AuthCredentialsServiceState is created, and a StorageDescription, pointing to that
* authentication description document.
*/
private DeferredResult createStorageDescription(
AzureInstanceContext ctx) {
String msg = "Getting Azure StorageAccountKeys for [" + ctx.storageAccount.name()
+ "] Storage Account";
AzureDeferredResultServiceCallback handler = new AzureDeferredResultServiceCallback(
this.service, msg) {
@Override
protected Throwable consumeError(Throwable exc) {
return new IllegalStateException(msg + ": FAILED with " + exc.getMessage(),
exc);
}
@Override
protected DeferredResult consumeSuccess(
StorageAccountListKeysResultInner body) {
logFine(() -> String.format(msg + ": SUCCESS"));
return DeferredResult.completed(body);
}
};
getStorageManagementClientImpl(ctx)
.storageAccounts()
.listKeysAsync(ctx.storageAccountRGName, ctx.storageAccount.name(), handler);
return handler.toDeferredResult()
.thenCompose(keys -> {
Operation createStorageDescOp = Operation
.createPost(getHost(), StorageDescriptionService.FACTORY_LINK)
.setBody(AzureUtils.constructStorageDescription(
getHost(), getSelfLink(),
ctx.storageAccount, ctx, keys))
.setReferer(getUri());
return sendWithDeferredResult(createStorageDescOp,
StorageDescription.class);
})
.thenCompose(storageDescription -> {
ctx.storageDescription = storageDescription;
ctx.bootDiskState.storageDescriptionLink = storageDescription.documentSelfLink;
return DeferredResult.completed(ctx.storageDescription);
});
}
@Override
protected String getProvisioningState(StorageAccountInner sa) {
ProvisioningState provisioningState = sa.provisioningState();
// For some reason SA.provisioningState is null, so consider it CREATING.
if (provisioningState == null) {
return ProvisioningState.CREATING.name();
}
return provisioningState.name();
}
@Override
protected Runnable checkProvisioningStateCall(
ServiceCallback checkProvisioningStateCallback) {
return () -> getStorageManagementClientImpl(this.ctx)
.storageAccounts()
.getByResourceGroupAsync(
this.ctx.storageAccountRGName,
this.ctx.storageAccountName,
checkProvisioningStateCallback);
}
}
/**
* Azure async callback implementation that transitions to the next stage of the
* AzureInstanceService state machine once over.
*/
private abstract class TransitionToCallback extends AzureAsyncCallback {
final AzureInstanceContext ctx;
/**
* The next stage of {@code AzureInstanceService} state machine to transition once over.
*/
final AzureInstanceStage nextStage;
/**
* The execution context of this call indicating whether it is executed single or in batch
* mode.
*/
final AzureCallContext callCtx;
/**
* Informative message to log while executing this call.
*/
final String msg;
/**
* Use this callback in case of single Azure async call.
*/
TransitionToCallback(
AzureInstanceContext ctx,
AzureInstanceStage nextStage,
String message) {
this(ctx, nextStage, AzureCallContext.newSingleCallContext(), message);
}
/**
* Use this callback in case of multiple/batch Azure async calls. It transitions to next
* stage when ALL calls have succeeded or transitions exceptionally upon FIRST error. If
* latter subsequent calls (either success or failure) are just ignored.
*/
TransitionToCallback(
AzureInstanceContext ctx,
AzureInstanceStage nextStage,
AzureCallContext azureCallContext,
String message) {
this.ctx = ctx;
this.nextStage = nextStage;
this.callCtx = azureCallContext;
this.msg = message;
AzureInstanceService.this.logFine(this.msg + ": STARTED");
}
@Override
public final void onError(Throwable e) {
if (this.callCtx.failOnError) {
if (this.callCtx.hasAnyFailed.compareAndSet(false, true)) {
// Check whether this is the first failure and proceed to next stage.
// i.e. fail-fast on batch operations.
AzureInstanceService.this.handleCloudError(
String.format("%s: FAILED. Details:", this.msg), this.ctx,
COMPUTE_NAMESPACE, e);
} else {
e = new IllegalStateException(this.msg + ": FAILED. Details: " + e.getMessage(),
e);
// Any subsequent failure is just logged.
AzureInstanceService.this.logSevere(e);
}
} else {
final String finalMsg = e.getMessage();
AzureInstanceService.this.logFine(() -> String.format("%s: SUCCESS with error."
+ " Details: %s", this.msg, finalMsg));
transition();
}
}
/**
* Hook that might be implemented by descendants to handle failed Azure call.
*
* Default error handling delegates to
* {@link AzureInstanceService#handleError(AzureInstanceContext, Throwable)}.
*/
void handleFailure(Throwable e) {
AzureInstanceService.this.handleError(this.ctx, e);
}
@Override
public final void onSuccess(T result) {
if (this.callCtx.hasAnyFailed.get()) {
AzureInstanceService.this.logFine(this.msg + ": SUCCESS. Still batch calls have "
+ "failed so SKIP this result.");
return;
}
AzureInstanceService.this.logFine(this.msg + ": SUCCESS");
// First delegate to descendants to process result body
CompletionStage handleSuccess = handleSuccess(result);
// Then transition upon completion
handleSuccess.whenComplete((body, exc) -> {
if (exc != null) {
handleFailure(exc);
} else {
transition();
}
});
}
/**
* Hook to be implemented by descendants to handle successful Azure call.
*
* The implementation should focus on consuming the result. It is responsibility of this
* class to handle transition to next stage as defined by
* {@link AzureInstanceService#handleAllocation(AzureInstanceContext, AzureInstanceStage)}.
*/
abstract CompletionStage handleSuccess(T resultBody);
/**
* Transition to the next stage of AzureInstanceService state machine once all Azure calls
* are completed.
*/
void transition() {
if (this.callCtx.numberOfCalls.decrementAndGet() == 0) {
// Check whether all calls have succeeded and proceed to next stage.
AzureInstanceService.this.handleAllocation(this.ctx, this.nextStage);
}
}
}
}