org.ligoj.app.plugin.vm.azure.VmAzurePluginResource.java Source code

Java tutorial

Introduction

Here is the source code for org.ligoj.app.plugin.vm.azure.VmAzurePluginResource.java

Source

/*
 * Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
 */
package org.ligoj.app.plugin.vm.azure;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.cache.annotation.CacheKey;
import javax.cache.annotation.CacheResult;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.apache.commons.lang3.StringUtils;
import org.ligoj.app.api.SubscriptionStatusWithData;
import org.ligoj.app.dao.NodeRepository;
import org.ligoj.app.plugin.vm.VmNetwork;
import org.ligoj.app.plugin.vm.VmResource;
import org.ligoj.app.plugin.vm.azure.AzureNic.AzureIpConfiguration;
import org.ligoj.app.plugin.vm.azure.AzureNic.AzureIpConfigurationProperties;
import org.ligoj.app.plugin.vm.azure.AzurePublicIp.AzureDns;
import org.ligoj.app.plugin.vm.azure.AzureVmList.AzureVmDetails;
import org.ligoj.app.plugin.vm.azure.AzureVmList.AzureVmEntry;
import org.ligoj.app.plugin.vm.azure.AzureVmList.AzureVmNicRef;
import org.ligoj.app.plugin.vm.azure.AzureVmList.AzureVmOs;
import org.ligoj.app.plugin.vm.dao.VmScheduleRepository;
import org.ligoj.app.plugin.vm.execution.VmExecutionServicePlugin;
import org.ligoj.app.plugin.vm.model.VmOperation;
import org.ligoj.app.plugin.vm.model.VmStatus;
import org.ligoj.bootstrap.core.resource.BusinessException;
import org.ligoj.bootstrap.core.security.SecurityHelper;
import org.ligoj.bootstrap.core.validation.ValidationJsonException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.extern.slf4j.Slf4j;

/**
 * Azure VM resource.
 */
@Path(VmAzurePluginResource.URL)
@Service
@Produces(MediaType.APPLICATION_JSON)
@Slf4j
public class VmAzurePluginResource extends AbstractAzureToolPluginResource implements VmExecutionServicePlugin {

    /**
     * Plug-in key.
     */
    public static final String URL = VmResource.SERVICE_URL + "/azure";

    /**
     * Plug-in key.
     */
    public static final String KEY = URL.replace('/', ':').substring(1);

    /**
     * VM operation POST URL.
     */
    public static final String OPERATION_VM = COMPUTE_URL + "/{vm}/{operation}?api-version={apiVersion}";

    /**
     * REST URL format for virtual machine URLs.
     */
    public static final String SIZES_URL = "subscriptions/{subscriptionId}/providers/Microsoft.Compute/locations/{location}/vmSizes?api-version={apiVersion}";

    /**
     * REST URL format for a VM.
     */
    public static final String VM_URL = COMPUTE_URL + "/{vm}?$expand=instanceView&api-version={apiVersion}";

    /**
     * The managed VM name, not the VM identifier (vmid). Note that VM identifier cannot be used to filter resources...
     * Nevertheless, both ID and name can be used to find a VM during the subscription.
     */
    public static final String PARAMETER_VM = KEY + ":name";

    private static final Map<VmOperation, String> OPERATION_TO_AZURE = new EnumMap<>(VmOperation.class);

    static {
        // OFF and SHUTDOWN are both, OS way first, then hard way.
        OPERATION_TO_AZURE.put(VmOperation.OFF, "powerOff");
        OPERATION_TO_AZURE.put(VmOperation.SHUTDOWN, "powerOff");

        OPERATION_TO_AZURE.put(VmOperation.ON, "start");

        // Restart and reboot are both, OS way first, then hard way.
        OPERATION_TO_AZURE.put(VmOperation.REBOOT, "restart");
        OPERATION_TO_AZURE.put(VmOperation.RESET, "restart");

        // No SUSPEND operation
    }

    /**
     * Mapping table giving the operation to execute depending on the requested operation and the status of the VM.
     * <TABLE summary="Mapping Table">
     * <THEAD>
     * <TR>
     * <TH>status</TH>
     * <TH>requested operation</TH>
     * <TH>executed operation</TH>
     * </TR>
     * </THEAD> <TBODY>
     * <TR>
     * <TD>off</TD>
     * <TD>shutdown</TD>
     * <TD>-</TD>
     * </TR>
     * <TR>
     * <TD>off</TD>
     * <TD>power off</TD>
     * <TD>-</TD>
     * </TR>
     * <TR>
     * <TD>off</TD>
     * <TD>resume</TD>
     * <TD>resume</TD>
     * </TR>
     * <TR>
     * <TD>off</TD>
     * <TD>suspend</TD>
     * <TD>-</TD>
     * </TR>
     * <TR>
     * <TD>off</TD>
     * <TD>reset</TD>
     * <TD>resume</TD>
     * </TR>
     * <TR>
     * <TD>off</TD>
     * <TD>restart</TD>
     * <TD>resume</TD>
     * </TR>
     * <TR>
     * <TD>on</TD>
     * <TD>shutdown</TD>
     * <TD>shutdown</TD>
     * </TR>
     * <TR>
     * <TD>on</TD>
     * <TD>power off</TD>
     * <TD>power off</TD>
     * </TR>
     * <TR>
     * <TD>on</TD>
     * <TD>resume</TD>
     * <TD>-</TD>
     * </TR>
     * <TR>
     * <TD>on</TD>
     * <TD>suspend</TD>
     * <TD>-</TD>
     * </TR>
     * <TR>
     * <TD>on</TD>
     * <TD>reset</TD>
     * <TD>reset</TD>
     * </TR>
     * <TR>
     * <TD>on</TD>
     * <TD>restart</TD>
     * <TD>restart</TD>
     * </TR>
     * </TBODY>
     * </TABLE>
     */
    private static final Map<VmStatus, Map<VmOperation, VmOperation>> FAILSAFE_OPERATIONS = new EnumMap<>(
            VmStatus.class);

    /**
     * Register a mapping Status+operation to operation.
     * 
     * @param status
     *            The current status.
     * @param operation
     *            The requested operation
     * @param operationFailSafe
     *            The computed operation.
     */
    private static void registerOperation(final VmStatus status, final VmOperation operation,
            final VmOperation operationFailSafe) {
        FAILSAFE_OPERATIONS.computeIfAbsent(status, s -> new EnumMap<>(VmOperation.class));
        FAILSAFE_OPERATIONS.get(status).put(operation, operationFailSafe);
    }

    /**
     * Busy provisioning code suffix, yes it's ugly, but lazy.
     */
    private static final String BUSY_CODE = "ing";
    /**
     * Undeployed provisioning code.
     */
    private static final String UNDEPLOYED_CODE = "PowerState/deallocated";

    /**
     * VM code to {@link VmStatus} mapping.
     * 
     * @see https://docs.microsoft.com/en-us/rest/api/compute/virtualmachines/virtualmachines-state
     */
    private static final Map<String, VmStatus> CODE_TO_STATUS = new HashMap<>();
    static {
        CODE_TO_STATUS.put("PowerState/running", VmStatus.POWERED_ON);
        CODE_TO_STATUS.put("PowerState/starting", VmStatus.POWERED_ON);
        CODE_TO_STATUS.put("PowerState/stopped", VmStatus.POWERED_OFF);
        CODE_TO_STATUS.put("PowerState/deallocated", VmStatus.POWERED_OFF);
        CODE_TO_STATUS.put("PowerState/deallocating", VmStatus.POWERED_OFF);
        CODE_TO_STATUS.put("PowerState/stopping", VmStatus.POWERED_OFF);
    }

    static {
        // Powered off status
        registerOperation(VmStatus.POWERED_OFF, VmOperation.ON, VmOperation.ON);
        registerOperation(VmStatus.POWERED_OFF, VmOperation.RESET, VmOperation.ON);
        registerOperation(VmStatus.POWERED_OFF, VmOperation.REBOOT, VmOperation.ON);

        // Powered on status
        registerOperation(VmStatus.POWERED_ON, VmOperation.SHUTDOWN, VmOperation.SHUTDOWN);
        registerOperation(VmStatus.POWERED_ON, VmOperation.OFF, VmOperation.OFF);
        registerOperation(VmStatus.POWERED_ON, VmOperation.RESET, VmOperation.RESET);
        registerOperation(VmStatus.POWERED_ON, VmOperation.REBOOT, VmOperation.REBOOT);
    }

    @Autowired
    private VmScheduleRepository vmScheduleRepository;

    @Autowired
    private SecurityHelper securityHelper;

    @Autowired
    private NodeRepository nodeRepository;

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * Used for "this" and forcing proxying.
     */
    @Autowired
    protected VmAzurePluginResource self;

    @Override
    public void link(final int subscription) throws Exception {
        // Validate the virtual machine name
        getAzureVm(subscriptionResource.getParameters(subscription));
    }

    /**
     * Find the virtual machines matching to the given criteria. Look into virtual machine name only.
     *
     * @param node
     *            the node to be tested with given parameters.
     * @param criteria
     *            the search criteria. Case is insensitive.
     * @return virtual machines.
     */
    @GET
    @Path("{node:service:.+}/{criteria}")
    @Consumes(MediaType.APPLICATION_JSON)
    public List<AzureVm> findAllByName(@PathParam("node") final String node,
            @PathParam("criteria") final String criteria) throws IOException {
        // Check the node exists
        if (nodeRepository.findOneVisible(node, securityHelper.getLogin()) == null) {
            return Collections.emptyList();
        }

        // Get all VMs and then filter by its name or id
        final Map<String, String> parameters = pvResource.getNodeParameters(node);
        final String vmJson = StringUtils.defaultString(getAzureResource(parameters, FIND_VM_URL),
                "{\"value\":[]}");
        final AzureVmList azure = objectMapper.readValue(vmJson, AzureVmList.class);
        return azure.getValue().stream().filter(vm -> StringUtils.containsIgnoreCase(vm.getName(), criteria))
                .map(v -> toVm(v, null)).sorted().collect(Collectors.toList());
    }

    /**
     * Return the fail-safe {@link VmSize} corresponding to the requested type.
     */
    private VmSize toVmSize(final Map<String, String> parameters, final String azSub, final String type,
            final String location) {
        try {
            return self.getInstanceSizes(azSub, location, parameters).getOrDefault(type, new VmSize(type));
        } catch (final IOException ioe) {
            // Unmanaged size for this subscription
            log.info("Unmanaged VM size {} : {}", type, ioe.getMessage());
            return new VmSize(type);
        }
    }

    /**
     * Build a described {@link AzureVm} completing the VM details with the instance details.
     * 
     * @param azureVm
     *            The Azure VM object built from the raw JSON stream.
     * @param sizeProvider
     *            Optional Azure instance size provider.
     * @return The merge VM details.
     */
    private AzureVm toVm(final AzureVmEntry azureVm, final BiFunction<String, String, VmSize> sizeProvider) {
        final AzureVm result = new AzureVm();
        final AzureVmDetails properties = azureVm.getProperties();
        result.setId(azureVm.getName()); // ID is the name for Azure
        result.setInternalId(properties.getVmId());
        result.setName(azureVm.getName());
        result.setLocation(azureVm.getLocation());

        // Instance type details if available
        if (sizeProvider != null) {
            final String instanceType = properties.getHardwareProfile().get("vmSize");
            final VmSize size = sizeProvider.apply(instanceType, azureVm.getLocation());
            result.setCpu(size.getNumberOfCores());
            result.setRam(size.getMemoryInMB());
        }

        // OS
        final AzureVmOs image = properties.getStorageProfile().getImageReference();
        if (image.getOffer() == null) {
            // From image
            result.setOs(properties.getStorageProfile().getOsDisk().getOsType() + " ("
                    + StringUtils.substringAfterLast(image.getId(), "/") + ")");
        } else {
            // From marketplace : provider
            result.setOs(image.getOffer() + " " + image.getSku() + " " + image.getPublisher());
        }

        // Disk size
        result.setDisk(properties.getStorageProfile().getOsDisk().getDiskSizeGB());

        return result;
    }

    @Override
    public AzureVm getVmDetails(final Map<String, String> parameters) {
        final String name = parameters.get(PARAMETER_VM);
        final AzureCurlProcessor processor = new AzureCurlProcessor();
        try {
            // Associate the oAuth token to the processor
            authenticate(parameters, processor);

            // Get the VM data
            final String vmJson = getVmResource(name, parameters, processor, VM_URL.replace("{vm}", name));

            // VM as been found, return the details with status
            final AzureVmEntry azure = readValue(vmJson, AzureVmEntry.class);

            // Get instance details
            final String azSub = parameters.get(PARAMETER_SUBSCRIPTION);
            final BiFunction<String, String, VmSize> sizes = (t, l) -> toVmSize(parameters, azSub, t, l);
            final AzureVm vm = toVmStatus(azure, sizes);
            vm.setNetworks(new ArrayList<>());

            // Get network data for each network references
            getNetworkDetails(name, parameters, processor,
                    azure.getProperties().getNetworkProfile().getNetworkInterfaces(), vm.getNetworks());
            return vm;
        } finally {
            processor.close();
        }
    }

    /**
     * Check the VM has been found with not <code>null</code> response.
     */
    private String checkResponse(final String name, final String vmJson) {
        if (vmJson == null) {
            // Invalid VM identifier? This VM cannot be found
            throw new ValidationJsonException(PARAMETER_VM, "azure-vm", name);
        }
        return vmJson;
    }

    /**
     * Fill the given VM with its network details.
     */
    private void getNetworkDetails(final String name, final Map<String, String> parameters,
            final AzureCurlProcessor processor, final Collection<AzureVmNicRef> nicRefs,
            final Collection<VmNetwork> networks) {
        nicRefs.stream().map(
                nicRef -> getVmResource(name, parameters, processor, nicRef.getId() + "?api-version=2017-09-01"))
                // Parse the NIC JSON data and get the details
                .forEach(nicJson -> getNicDetails(name, parameters, processor, readValue(nicJson, AzureNic.class),
                        networks));
    }

    private String getVmResource(final String name, final Map<String, String> parameters,
            final AzureCurlProcessor processor, final String resource) {
        return checkResponse(name, execute(processor, "GET", buildUrl(parameters, resource), ""));
    }

    /**
     * Fill the given VM with its network details.
     */
    private void getNicDetails(final String name, final Map<String, String> parameters,
            final AzureCurlProcessor processor, final AzureNic nic, final Collection<VmNetwork> networks) {
        // Extract the direct private IP and the indirect public IP
        nic.getProperties().getIpConfigurations().stream().map(AzureIpConfiguration::getProperties)

                // Save the private IP
                .peek(c -> networks.add(new VmNetwork("private", c.getPrivateIPAddress(), null)))

                // Check there is an attached public IP
                .map(AzureIpConfigurationProperties::getPublicIPAddress).filter(Objects::nonNull)

                // Get the public IP json
                .map(id -> getVmResource(name, parameters, processor, id.getId() + "?api-version=2017-09-01"))

                // Parse the public IP JSON data
                .map(i -> readValue(i, AzurePublicIp.class)).map(AzurePublicIp::getProperties)

                // Get the public IP and the optional DNS
                .forEach(i -> networks.add(new VmNetwork("public", i.getIpAddress(),
                        Optional.ofNullable(i.getDnsSettings()).map(AzureDns::getFqdn).orElse(null))));
    }

    /**
     * Parse the String and return a runtime exception whn is not a correct JSON.
     */
    private <T> T readValue(final String json, final Class<T> clazz) {
        try {
            return objectMapper.readValue(json, clazz);
        } catch (final IOException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Validate and return the {@link AzureVmEntry} without instance details.
     * 
     * @param parameters
     *            the space parameters.
     * @return Azure Virtual Machine description.
     */
    protected AzureVmEntry getAzureVm(final Map<String, String> parameters) throws IOException {

        // Get all VMs and then filter by its name or id
        final String name = parameters.get(PARAMETER_VM);
        final String vmJson = checkResponse(name, getAzureResource(parameters, VM_URL.replace("{vm}", name)));

        // VM as been found, return the details with status
        return objectMapper.readValue(vmJson, AzureVmEntry.class);
    }

    /**
     * Build a described {@link AzureVm} bean the JSON VM instance view.
     */
    private AzureVm toVmStatus(final AzureVmEntry azureVm, BiFunction<String, String, VmSize> sizeProvider) {
        final AzureVm result = toVm(azureVm, sizeProvider);
        final AzureVmDetails properties = azureVm.getProperties();

        // State
        final List<AzureVmList.VmStatus> statuses = properties.getInstanceView().getStatuses();
        result.setStatus(getStatus(statuses));
        result.setBusy(isBusy(statuses));
        result.setDeployed(isDeployed(statuses));
        return result;
    }

    /**
     * Return the generic VM status from the Azure statuses
     */
    private VmStatus getStatus(final List<AzureVmList.VmStatus> statuses) {
        return statuses.stream().map(AzureVmList.VmStatus::getCode).map(CODE_TO_STATUS::get)
                .filter(Objects::nonNull).findFirst().orElse(null);
    }

    /**
     * Return the generic VM status from the Azure statuses
     */
    private boolean isBusy(final List<AzureVmList.VmStatus> statuses) {
        return statuses.stream().map(AzureVmList.VmStatus::getCode).filter(c -> c.startsWith("ProvisioningState"))
                .anyMatch(c -> c.endsWith(BUSY_CODE));
    }

    /**
     * Return the generic VM status from the Azure statuses
     */
    private boolean isDeployed(final List<AzureVmList.VmStatus> statuses) {
        return statuses.stream().map(AzureVmList.VmStatus::getCode).anyMatch(UNDEPLOYED_CODE::equals);
    }

    @Override
    protected String buildUrl(final Map<String, String> parameters, final String resource) {
        return super.buildUrl(parameters, resource).replace("{vm}", parameters.getOrDefault(PARAMETER_VM, "-"));
    }

    @Override
    public String getKey() {
        return VmAzurePluginResource.KEY;
    }

    @Override
    public SubscriptionStatusWithData checkSubscriptionStatus(final int subscription, final String node,
            final Map<String, String> parameters) {
        final SubscriptionStatusWithData status = new SubscriptionStatusWithData();
        status.put("vm", getVmDetails(parameters));
        status.put("schedules", vmScheduleRepository.countBySubscription(subscription));
        return status;
    }

    @Override
    public void execute(final int subscription, final VmOperation operation) {
        final Map<String, String> parameters = subscriptionResource.getParametersNoCheck(subscription);

        // First get VM state
        final AzureVm vm = getVmDetails(parameters);
        final VmStatus status = vm.getStatus();

        // Get the right operation depending on the current state
        final VmOperation operationF = failSafeOperation(status, operation);
        if (operationF == null) {
            // Final operation is considered as useless
            log.info("Requested operation {} is marked as useless considering the status {} of vm {}", operation,
                    status, parameters.get(PARAMETER_VM));
            return;
        }

        // Execute the operation
        checkSchedulerResponse(authenticateAndExecute(parameters, HttpMethod.POST,
                OPERATION_VM.replace("{operation}", OPERATION_TO_AZURE.get(operationF))));
    }

    /**
     * Check the response is valid. For now, the response must not be <code>null</code>.
     */
    private void checkSchedulerResponse(final String response) {
        if (response == null) {
            // The result is not correct
            throw new BusinessException("vm-operation-execute");
        }
    }

    /**
     * Decide the best operation suiting to the required operation and depending on the current status of the virtual
     * machine.
     * 
     * @param status
     *            The current status of the VM.
     * @param operation
     *            The requested operation.
     * @return The fail-safe operation suiting to the current status of the VM. Return <code>null</code> when the
     *         computed operation is irrelevant.
     */
    protected VmOperation failSafeOperation(final VmStatus status, final VmOperation operation) {
        return Optional.ofNullable(FAILSAFE_OPERATIONS.get(status)).map(m -> m.get(operation)).orElse(null);
    }

    /**
     * Return the available Azure sizes.
     * 
     * @param azSub
     *            The related Azure subscription identifier. Seem to duplicate the one inside the given parameters, but
     *            required for the cache key.
     * @param location
     *            The target location, required by Azure web service
     * @param parameters
     *            The credentials parameters
     */
    @CacheResult(cacheName = "azure-sizes")
    public Map<String, VmSize> getInstanceSizes(@CacheKey final String azSub, @CacheKey final String location,
            final Map<String, String> parameters) throws IOException {
        final String jsonSizes = getAzureResource(parameters,
                SIZES_URL.replace("{subscriptionId}", azSub).replace("{location}", location));
        return objectMapper.readValue(StringUtils.defaultString(jsonSizes, "{\"value\":[]}"), VmSizes.class)
                .getValue().stream().collect(Collectors.toMap(VmSize::getName, Function.identity()));
    }
}