org.ow2.proactive.connector.iaas.cloud.provider.azure.AzureProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.ow2.proactive.connector.iaas.cloud.provider.azure.AzureProvider.java

Source

/*
 * ProActive Parallel Suite(TM):
 * The Open Source library for parallel and distributed
 * Workflows & Scheduling, Orchestration, Cloud Automation
 * and Big Data Analysis on Enterprise Grids & Clouds.
 *
 * Copyright (c) 2007 - 2017 ActiveEon
 * Contact: contact@activeeon.com
 *
 * This library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation: version 3 of
 * the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * If needed, contact us to obtain a release under GPL Version 2 or 3
 * or a different license than the AGPL.
 */
package org.ow2.proactive.connector.iaas.cloud.provider.azure;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.apache.log4j.Logger;
import org.ow2.proactive.connector.iaas.cloud.provider.CloudProvider;
import org.ow2.proactive.connector.iaas.model.Hardware;
import org.ow2.proactive.connector.iaas.model.Image;
import org.ow2.proactive.connector.iaas.model.Infrastructure;
import org.ow2.proactive.connector.iaas.model.Instance;
import org.ow2.proactive.connector.iaas.model.InstanceCredentials;
import org.ow2.proactive.connector.iaas.model.InstanceScript;
import org.ow2.proactive.connector.iaas.model.Options;
import org.ow2.proactive.connector.iaas.model.ScriptResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.google.common.collect.Lists;
import com.microsoft.azure.management.Azure;
import com.microsoft.azure.management.compute.OperatingSystemTypes;
import com.microsoft.azure.management.compute.VirtualMachine;
import com.microsoft.azure.management.compute.VirtualMachineCustomImage;
import com.microsoft.azure.management.compute.VirtualMachineExtension;
import com.microsoft.azure.management.compute.VirtualMachineSizeTypes;
import com.microsoft.azure.management.network.Network;
import com.microsoft.azure.management.network.NetworkInterface;
import com.microsoft.azure.management.network.NetworkSecurityGroup;
import com.microsoft.azure.management.network.NicIpConfiguration;
import com.microsoft.azure.management.network.PublicIpAddress;
import com.microsoft.azure.management.network.model.HasPrivateIpAddress;
import com.microsoft.azure.management.resources.ResourceGroup;
import com.microsoft.azure.management.resources.fluentcore.arm.Region;
import com.microsoft.azure.management.resources.fluentcore.model.Creatable;
import com.microsoft.azure.management.resources.fluentcore.utils.SdkContext;

import lombok.Getter;

/**
 * Provides Microsoft Azure clouds' management using the official java SDK.
 *
 * This class has been tested by ActiveEon to be thread safe (using Azure SDK release version 1.0.0-beta5).
 * However this need to be carefully double-checked after every SDK upgrades, as mentioned by Microsoft:
 * ------------------------------------------------------------------------------------------------------------
 * We do not make any thread-safety guarantees about our libraries. We also do not test them for thread-safety.
 * Methods that are currently thread-safe may be thread-unsafe in future versions.
 * ------------------------------------------------------------------------------------------------------------
 *
 * @author ActiveEon Team
 * @since 01/03/17
 */
@Component
public class AzureProvider implements CloudProvider {

    private final Logger logger = Logger.getLogger(AzureProvider.class);

    @Getter
    private final String type = "azure";

    private static final VirtualMachineSizeTypes DEFAULT_VM_SIZE = VirtualMachineSizeTypes.STANDARD_D1_V2;

    private static final int RESOURCES_NAME_EXTRA_CHARS = 10;

    private static final String VIRTUAL_NETWORK_NAME_BASE = "vnet";

    private static final String PUBLIC_IP_ADDRESS_NAME_BASE = "ip";

    private static final String NETWORK_SECURITY_GROUP_NAME_BASE = "sg";

    private static final String NETWORK_INTERFACE_NAME_BASE = "if";

    private static final String OS_DISK_NAME_BASE = "os";

    private static final String DEFAULT_USERNAME = "activeeon";

    private static final String DEFAULT_PASSWORD = "Act1v0N";

    private static final String DEFAULT_PRIVATE_NETWORK_CIDR = "10.0.0.0/24";

    private static final Boolean DEFAULT_STATIC_PUBLIC_IP = true;

    private static final String SCRIPT_EXTENSION_PUBLISHER = "Microsoft.Azure.Extensions";

    private static final String SCRIPT_EXTENSION_TYPE = "CustomScript";

    private static final String SCRIPT_EXTENSION_VERSION = "2.0";

    private static final String SCRIPT_EXTENSION_CMD_KEY = "commandToExecute";

    private static final String SCRIPT_SEPARATOR = "&&";

    private static final String SINGLE_INSTANCE_NUMBER = "1";

    @Autowired
    private AzureServiceCache azureServiceCache;

    @Autowired
    private AzureProviderUtils azureProviderUtils;

    @Override
    public Set<Instance> createInstance(Infrastructure infrastructure, Instance instance) {

        Azure azureService = azureServiceCache.getService(infrastructure);
        String instanceTag = Optional.ofNullable(instance.getTag()).orElseThrow(
                () -> new RuntimeException("ERROR missing instance tag/name from instance: '" + instance + "'"));

        // Check for Image by name first and then by id
        String imageNameOrId = Optional.ofNullable(instance.getImage()).orElseThrow(
                () -> new RuntimeException("ERROR missing Image name/id from instance: '" + instance + "'"));
        VirtualMachineCustomImage image = getImageByName(azureService, imageNameOrId)
                .orElseGet(() -> getImageById(azureService, imageNameOrId).orElseThrow(() -> new RuntimeException(
                        "ERROR unable to find custom Image: '" + instance.getImage() + "'")));

        // Get the options (Optional by design)
        Optional<Options> options = Optional.ofNullable(instance.getOptions());

        // Try to retrieve the resourceGroup from provided name, otherwise get it from image
        ResourceGroup resourceGroup = azureProviderUtils
                .searchResourceGroupByName(azureService,
                        options.map(Options::getResourceGroup).orElseGet(image::resourceGroupName))
                .orElseThrow(() -> new RuntimeException(
                        "ERROR unable to find a suitable resourceGroup from instance: '" + instance + "'"));

        // Try to get region from provided name, otherwise get it from image
        Region region = options.map(presentOptions -> Region.findByLabelOrName(presentOptions.getRegion()))
                .orElseGet(image::region);

        // Prepare a new virtual private network (same for all VMs)
        Optional<String> optionalPrivateNetworkCIDR = options.map(Options::getPrivateNetworkCIDR);
        Creatable<Network> creatableVirtualNetwork = azureProviderUtils.prepareVirtualNetwork(azureService, region,
                resourceGroup, createUniqueVirtualNetworkName(instanceTag),
                optionalPrivateNetworkCIDR.orElse(DEFAULT_PRIVATE_NETWORK_CIDR));

        // Prepare a new  security group (same for all VMs)
        Creatable<NetworkSecurityGroup> creatableNetworkSecurityGroup = azureProviderUtils
                .prepareSSHNetworkSecurityGroup(azureService, region, resourceGroup,
                        createUniqueSecurityGroupName(instance.getTag()));

        // Prepare the VM(s)
        Optional<Boolean> optionalStaticPublicIP = options.map(Options::getStaticPublicIP);
        List<Creatable<VirtualMachine>> creatableVirtualMachines = IntStream
                .rangeClosed(1,
                        Integer.valueOf(Optional.ofNullable(instance.getNumber()).orElse(SINGLE_INSTANCE_NUMBER)))
                .mapToObj(instanceNumber -> {
                    // Create a new public IP address (one per VM)
                    String publicIPAddressName = createUniquePublicIPName(
                            createUniqueInstanceTag(instanceTag, instanceNumber));
                    Creatable<PublicIpAddress> creatablePublicIPAddress = azureProviderUtils.preparePublicIPAddress(
                            azureService, region, resourceGroup, publicIPAddressName,
                            optionalStaticPublicIP.orElse(DEFAULT_STATIC_PUBLIC_IP));

                    // Prepare a new network interface (one per VM)
                    String networkInterfaceName = createUniqueNetworkInterfaceName(
                            createUniqueInstanceTag(instanceTag, instanceNumber));
                    Creatable<NetworkInterface> creatableNetworkInterface = azureProviderUtils
                            .prepareNetworkInterfaceFromScratch(azureService, region, resourceGroup,
                                    networkInterfaceName, creatableVirtualNetwork, creatableNetworkSecurityGroup,
                                    creatablePublicIPAddress);

                    return prepareVirtualMachine(instance, azureService, resourceGroup, region,
                            createUniqueInstanceTag(instanceTag, instanceNumber), image, creatableNetworkInterface);
                }).collect(Collectors.toList());

        // Create all VMs in parallel and collect IDs
        return azureService.virtualMachines().create(creatableVirtualMachines).values().stream()
                .map(vm -> instance.withTag(vm.name()).withId(vm.vmId()).withNumber(SINGLE_INSTANCE_NUMBER))
                .collect(Collectors.toSet());
    }

    private Optional<VirtualMachineCustomImage> getImageByName(Azure azureService, String name) {
        return azureService.virtualMachineCustomImages().list().stream()
                .filter(customImage -> customImage.name().equals(name)).findAny();
    }

    private Optional<VirtualMachineCustomImage> getImageById(Azure azureService, String id) {
        return azureService.virtualMachineCustomImages().list().stream()
                .filter(customImage -> customImage.id().equals(id)).findAny();
    }

    private Creatable<VirtualMachine> prepareVirtualMachine(Instance instance, Azure azureService,
            ResourceGroup resourceGroup, Region region, String instanceTag, VirtualMachineCustomImage image,
            Creatable<NetworkInterface> creatableNetworkInterface) {

        // Configure the VM depending on the OS type
        VirtualMachine.DefinitionStages.WithFromImageCreateOptionsManaged creatableVirtualMachineWithImage;
        OperatingSystemTypes operatingSystemType = image.osDiskImage().osType();
        if (operatingSystemType.equals(OperatingSystemTypes.LINUX)) {
            creatableVirtualMachineWithImage = configureLinuxVirtualMachine(azureService, instanceTag, region,
                    resourceGroup, instance.getCredentials(), image, creatableNetworkInterface);
        } else if (operatingSystemType.equals(OperatingSystemTypes.WINDOWS)) {
            creatableVirtualMachineWithImage = configureWindowsVirtualMachine(azureService, instanceTag, region,
                    resourceGroup, instance.getCredentials(), image, creatableNetworkInterface);
        } else {
            throw new RuntimeException(
                    "ERROR Operating System of type '" + operatingSystemType.toString() + "' is not yet supported");
        }

        // Set VM size (or type) and name of OS' disk
        Optional<String> optionalHardwareType = Optional.ofNullable(instance.getHardware()).map(Hardware::getType);
        VirtualMachine.DefinitionStages.WithCreate creatableVMWithSize = creatableVirtualMachineWithImage
                .withSize(new VirtualMachineSizeTypes(optionalHardwareType.orElse(DEFAULT_VM_SIZE.toString())))
                .withOsDiskName(createUniqOSDiskName(instanceTag));

        // Add init script(s) using dedicated Microsoft extension
        Optional.ofNullable(instance.getInitScript()).map(InstanceScript::getScripts).ifPresent(scripts -> {
            if (scripts.length > 0) {
                StringBuilder concatenatedScripts = new StringBuilder();
                Lists.newArrayList(scripts)
                        .forEach(script -> concatenatedScripts.append(script).append(SCRIPT_SEPARATOR));
                creatableVMWithSize.defineNewExtension(createUniqueScriptName(instanceTag))
                        .withPublisher(SCRIPT_EXTENSION_PUBLISHER).withType(SCRIPT_EXTENSION_TYPE)
                        .withVersion(SCRIPT_EXTENSION_VERSION).withMinorVersionAutoUpgrade()
                        .withPublicSetting(SCRIPT_EXTENSION_CMD_KEY, concatenatedScripts.toString()).attach();
            }
        });
        return creatableVMWithSize;
    }

    private VirtualMachine.DefinitionStages.WithLinuxCreateManaged configureLinuxVirtualMachine(Azure azureService,
            String instanceTag, Region region, ResourceGroup resourceGroup, InstanceCredentials instanceCredentials,
            VirtualMachineCustomImage image, Creatable<NetworkInterface> creatableNetworkInterface) {
        // Retrieve optional credentials
        Optional<String> optionalUsername = Optional.ofNullable(instanceCredentials)
                .map(InstanceCredentials::getUsername);
        Optional<String> optionalPassword = Optional.ofNullable(instanceCredentials)
                .map(InstanceCredentials::getPassword);
        Optional<String> optionalPublicKey = Optional.ofNullable(instanceCredentials)
                .map(InstanceCredentials::getPublicKey);

        // Prepare the VM without credentials
        VirtualMachine.DefinitionStages.WithLinuxRootPasswordOrPublicKeyManaged creatableVMWithoutCredentials = azureService
                .virtualMachines().define(instanceTag).withRegion(region).withExistingResourceGroup(resourceGroup)
                .withNewPrimaryNetworkInterface(creatableNetworkInterface).withLinuxCustomImage(image.id())
                .withRootUsername(optionalUsername.orElse(DEFAULT_USERNAME));

        // Set the credentials (whether password or SSH key)
        return optionalPublicKey.map(creatableVMWithoutCredentials::withSsh).orElseGet(
                () -> creatableVMWithoutCredentials.withRootPassword(optionalPassword.orElse(DEFAULT_PASSWORD)));
    }

    private VirtualMachine.DefinitionStages.WithWindowsCreateManaged configureWindowsVirtualMachine(
            Azure azureService, String instanceTag, Region region, ResourceGroup resourceGroup,
            InstanceCredentials instanceCredentials, VirtualMachineCustomImage image,
            Creatable<NetworkInterface> creatableNetworkInterface) {
        // Retrieve optional credentials
        Optional<String> optionalUsername = Optional.ofNullable(instanceCredentials)
                .map(InstanceCredentials::getUsername);
        Optional<String> optionalPassword = Optional.ofNullable(instanceCredentials)
                .map(InstanceCredentials::getPassword);

        // Prepare the VM with credentials
        return azureService.virtualMachines().define(instanceTag).withRegion(region)
                .withExistingResourceGroup(resourceGroup).withNewPrimaryNetworkInterface(creatableNetworkInterface)
                .withWindowsCustomImage(image.id()).withAdminUsername(optionalUsername.orElse(DEFAULT_USERNAME))
                .withAdminPassword(optionalPassword.orElse(DEFAULT_PASSWORD));
    }

    /**
     * Create a unique tag for a VM based on the original tag provided and the instance index
     *
     * @param tagBase       the tag base
     * @param instanceIndex the instance index
     * @return a unique VM tag
     */
    private static String createUniqueInstanceTag(String tagBase, int instanceIndex) {
        if (instanceIndex > 1) {
            return tagBase + String.valueOf(instanceIndex);
        }
        return tagBase;
    }

    private static String createUniqueSecurityGroupName(String instanceTag) {
        return createUniqueName(instanceTag, NETWORK_SECURITY_GROUP_NAME_BASE);
    }

    private static String createUniqueVirtualNetworkName(String instanceTag) {
        return createUniqueName(instanceTag, VIRTUAL_NETWORK_NAME_BASE);
    }

    private static String createUniqueNetworkInterfaceName(String instanceTag) {
        return createUniqueName(instanceTag, NETWORK_INTERFACE_NAME_BASE);
    }

    private static String createUniquePublicIPName(String instanceTag) {
        return createUniqueName(instanceTag, PUBLIC_IP_ADDRESS_NAME_BASE);
    }

    private static String createUniqOSDiskName(String instanceTag) {
        return createUniqueName(instanceTag, OS_DISK_NAME_BASE);
    }

    private static String createUniqueScriptName(String instanceTag) {
        return createUniqueName(instanceTag, "");
    }

    private static String createUniqueName(String customPart, String basePart) {
        return SdkContext.randomResourceName(customPart + '-' + basePart,
                customPart.length() + basePart.length() + 1 + RESOURCES_NAME_EXTRA_CHARS);
    }

    @Override
    public void deleteInstance(Infrastructure infrastructure, String instanceId) {
        Azure azureService = azureServiceCache.getService(infrastructure);

        VirtualMachine vm = azureProviderUtils.searchVirtualMachineByID(azureService, instanceId).orElseThrow(
                () -> new RuntimeException("ERROR unable to find instance with ID: '" + instanceId + "'"));

        // Retrieve all resources attached to the instance
        NetworkInterface networkInterface = vm.getPrimaryNetworkInterface();
        com.microsoft.azure.management.network.Network network = networkInterface.primaryIpConfiguration()
                .getNetwork();
        NetworkSecurityGroup networkSecurityGroup = networkInterface.getNetworkSecurityGroup();
        Optional<PublicIpAddress> optionalPublicIPAddress = Optional.ofNullable(vm.getPrimaryPublicIpAddress());
        String osDiskID = vm.osDiskId();

        // Delete the VM first
        azureService.virtualMachines().deleteById(vm.id());

        // Then delete its network interface
        azureService.networkInterfaces().deleteById(networkInterface.id());

        // Delete its public IP address if present
        optionalPublicIPAddress.ifPresent(pubIPAddr -> azureService.publicIpAddresses().deleteById(pubIPAddr.id()));

        // Delete its main disk (OS), and keep data disks
        azureService.disks().deleteById(osDiskID);

        // Delete the security group if present and not attached to any network interface
        if (azureService.networkInterfaces().list().stream().map(NetworkInterface::getNetworkSecurityGroup)
                .filter(netSecGrp -> Optional.ofNullable(netSecGrp).isPresent())
                .noneMatch(netSecGrp -> netSecGrp.id().equals(networkSecurityGroup.id()))) {
            azureService.networkSecurityGroups().deleteById(networkSecurityGroup.id());
        }

        // Delete the virtual network if not attached to any network interface
        if (azureService.networkInterfaces().list().stream().map(NetworkInterface::primaryIpConfiguration)
                .filter(ipConf -> Optional.ofNullable(ipConf).isPresent()).map(NicIpConfiguration::getNetwork)
                .filter(net -> Optional.ofNullable(net).isPresent())
                .noneMatch(net -> net.id().equals(network.id()))) {
            azureService.networks().deleteById(network.id());
        }
    }

    @Override
    public Set<Instance> getAllInfrastructureInstances(Infrastructure infrastructure) {
        Azure azureService = azureServiceCache.getService(infrastructure);
        return azureProviderUtils.getAllVirtualMachines(azureService).stream().map(vm -> Instance.builder()
                .id(vm.vmId()).tag(vm.name()).number(SINGLE_INSTANCE_NUMBER)
                .hardware(Hardware.builder().type(vm.size().toString()).build())
                .network(org.ow2.proactive.connector.iaas.model.Network.builder().publicAddresses(vm
                        .networkInterfaceIds().stream()
                        .map(networkInterfaceId -> azureService.networkInterfaces().getById(networkInterfaceId))
                        .map(NetworkInterface::primaryIpConfiguration)
                        .filter(nicIpConfiguration -> Optional.ofNullable(nicIpConfiguration.getPublicIpAddress())
                                .isPresent())
                        .map(nicIpConfiguration -> nicIpConfiguration.getPublicIpAddress().ipAddress())
                        .collect(Collectors.toList()))
                        .privateAddresses(vm.networkInterfaceIds().stream()
                                .map(networkInterfaceId -> azureService.networkInterfaces()
                                        .getById(networkInterfaceId))
                                .flatMap(networkInterface -> networkInterface.ipConfigurations().values().stream())
                                .filter(nicIpConfiguration -> Optional
                                        .ofNullable(nicIpConfiguration.privateIpAddress()).isPresent())
                                .map(HasPrivateIpAddress::privateIpAddress).collect(Collectors.toList()))
                        .build())
                .status(String.valueOf(vm.powerState().toString())).build()).collect(Collectors.toSet());
    }

    @Override
    public List<ScriptResult> executeScriptOnInstanceId(Infrastructure infrastructure, String instanceId,
            InstanceScript instanceScript) {
        VirtualMachine vm = azureProviderUtils
                .searchVirtualMachineByID(azureServiceCache.getService(infrastructure), instanceId).orElseThrow(
                        () -> new RuntimeException("ERROR unable to find instance with ID: '" + instanceId + "'"));
        return executeScriptOnVM(vm, instanceScript);
    }

    @Override
    public List<ScriptResult> executeScriptOnInstanceTag(Infrastructure infrastructure, String instanceTag,
            InstanceScript instanceScript) {
        VirtualMachine vm = azureProviderUtils
                .searchVirtualMachineByName(azureServiceCache.getService(infrastructure), instanceTag)
                .orElseThrow(() -> new RuntimeException(
                        "ERROR unable to find instance with name: '" + instanceTag + "'"));
        return executeScriptOnVM(vm, instanceScript);
    }

    private List<ScriptResult> executeScriptOnVM(VirtualMachine vm, InstanceScript instanceScript) {

        // Concatenate all provided scripts in one (Multiple VMExtensions per handler not supported)
        StringBuilder concatenatedScripts = new StringBuilder();
        Arrays.stream(instanceScript.getScripts()).forEach(script -> {
            concatenatedScripts.append(script).append(SCRIPT_SEPARATOR);
        });

        Optional<VirtualMachineExtension> vmExtension = vm.extensions().values().stream()
                .filter(extension -> extension.publisherName().equals(SCRIPT_EXTENSION_PUBLISHER)
                        && extension.typeName().equals(SCRIPT_EXTENSION_TYPE))
                .findAny();
        if (vmExtension.isPresent()) {
            vm.update().updateExtension(vmExtension.get().name())
                    .withPublicSetting(SCRIPT_EXTENSION_CMD_KEY, concatenatedScripts.toString()).parent().apply();
        } else {
            vm.update().defineNewExtension(createUniqueScriptName(vm.name()))
                    .withPublisher(SCRIPT_EXTENSION_PUBLISHER).withType(SCRIPT_EXTENSION_TYPE)
                    .withVersion(SCRIPT_EXTENSION_VERSION).withMinorVersionAutoUpgrade()
                    .withPublicSetting(SCRIPT_EXTENSION_CMD_KEY, concatenatedScripts.toString()).attach().apply();
        }

        // Unable to retrieve scripts output, returns empty results instead
        return IntStream.rangeClosed(1, instanceScript.getScripts().length)
                .mapToObj(scriptNumber -> new ScriptResult(vm.vmId(), "", "")).collect(Collectors.toList());
    }

    @Override
    public Set<Image> getAllImages(Infrastructure infrastructure) {
        return azureServiceCache.getService(infrastructure).virtualMachineCustomImages().list().stream()
                .map(azureImage -> Image.builder().id(azureImage.id()).name(azureImage.name()).build())
                .collect(Collectors.toSet());
    }

    @Override
    public void deleteInfrastructure(Infrastructure infrastructure) {
        azureServiceCache.removeService(infrastructure);
    }

    @Override
    public String addToInstancePublicIp(Infrastructure infrastructure, String instanceId) {
        Azure azureService = azureServiceCache.getService(infrastructure);
        VirtualMachine vm = azureProviderUtils.searchVirtualMachineByID(azureService, instanceId).orElseThrow(
                () -> new RuntimeException("ERROR unable to find instance with ID: '" + instanceId + "'"));
        ResourceGroup resourceGroup = azureService.resourceGroups().getByName(vm.resourceGroupName());

        // Retrieve all resources attached to the instance
        NetworkInterface networkInterface = vm.getPrimaryNetworkInterface();
        com.microsoft.azure.management.network.Network network = networkInterface.primaryIpConfiguration()
                .getNetwork();
        NetworkSecurityGroup networkSecurityGroup = networkInterface.getNetworkSecurityGroup();
        Optional<PublicIpAddress> optionalPublicIPAddress = Optional.ofNullable(vm.getPrimaryPublicIpAddress());

        // Create a new public IP address
        PublicIpAddress newPublicIPAddress = azureProviderUtils.preparePublicIPAddress(azureService, vm.region(),
                resourceGroup, createUniquePublicIPName(vm.name()), DEFAULT_STATIC_PUBLIC_IP).create();

        // If primary network interface already has a public IP, then create a new/secondary network interface
        if (optionalPublicIPAddress.isPresent()) {
            vm.update()
                    .withNewSecondaryNetworkInterface(azureProviderUtils.prepareNetworkInterface(azureService,
                            vm.region(), resourceGroup, createUniqueNetworkInterfaceName(vm.name()), network,
                            networkSecurityGroup, newPublicIPAddress))
                    .apply();
        } else {
            vm.getPrimaryNetworkInterface().update().withExistingPrimaryPublicIpAddress(newPublicIPAddress).apply();
        }

        return newPublicIPAddress.ipAddress();
    }

    @Override
    public void removeInstancePublicIp(Infrastructure infrastructure, String instanceId) {
        Azure azureService = azureServiceCache.getService(infrastructure);
        VirtualMachine vm = azureProviderUtils.searchVirtualMachineByID(azureService, instanceId).orElseThrow(
                () -> new RuntimeException("ERROR unable to find instance with ID: '" + instanceId + "'"));

        // If there are secondary interfaces then remove one of them including its public IP address
        Optional<NetworkInterface> optionalSecondaryNetworkInterface = vm.networkInterfaceIds().stream()
                .map(networkInterfaceId -> azureService.networkInterfaces().getById(networkInterfaceId))
                .filter(networkInterface -> Optional
                        .ofNullable(networkInterface.primaryIpConfiguration().getPublicIpAddress()).isPresent())
                .filter(networkInterface -> !networkInterface.id().equals(vm.getPrimaryPublicIpAddressId()))
                .findAny();
        if (optionalSecondaryNetworkInterface.isPresent()) {
            PublicIpAddress publicIPAddress = optionalSecondaryNetworkInterface.get().primaryIpConfiguration()
                    .getPublicIpAddress();
            optionalSecondaryNetworkInterface.get().update().withoutPrimaryPublicIpAddress().apply();
            azureService.networkInterfaces().deleteById(optionalSecondaryNetworkInterface.get().id());
            azureService.publicIpAddresses().deleteById(publicIPAddress.id());
        }
        // Otherwise remove the public IP address from the primary interface, if present
        else if (Optional.ofNullable(vm.getPrimaryPublicIpAddress()).isPresent()) {
            PublicIpAddress publicIPAddress = vm.getPrimaryPublicIpAddress();
            vm.getPrimaryNetworkInterface().update().withoutPrimaryPublicIpAddress().apply();
            azureService.publicIpAddresses().deleteById(publicIPAddress.id());
        }
    }
}