Como criar solicitações de API e processar respostas

Neste documento, há detalhes sobre como construir solicitações de API e administrar respostas da API da API Compute Engine. Confira como: Ele aborda como:

  • construir um corpo de solicitação;
  • determinar os URIs de recursos necessários para uma solicitação;
  • processar respostas de API;
  • Determine se uma solicitação de API foi bem-sucedida.

Este documento não aborda como autenticar na API. Para saber como autenticar na API, leia Autenticar no Compute Engine.

Antes de começar

Como criar uma solicitação de API

A API Compute Engine exige solicitações de API em formato JSON. Para fazer uma solicitação de API, faça uma solicitação HTTP direta usando ferramentas como curl ou httplib2, ou use uma das bibliotecas de cliente disponíveis.

Quando você faz uma solicitação de API que requer um corpo, como uma solicitação POST, UPDATE ou PATCH, o corpo da solicitação contém as propriedades de recurso que você quer definir nesta solicitação. Por exemplo, o comando curl a seguir faz uma solicitação POST ao URI de recurso de instâncias. A solicitação cria uma instância com as propriedades definidas no corpo da solicitação. O corpo da solicitação é indicado pela sinalização -d:

curl -X POST -H "Authorization: Bearer [OAUTH_TOKEN]" -H "Content-Type: application/json" \
https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances -d \
'{
  "disks":[
    {
      "boot":"true",
      "initializeParams":{
        "sourceImage":"https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-10-buster-v20210122"
      }
    }
  ],
  "machineType":"https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/machineTypes/e2-standard-2",
  "name":"VM_NAME",
  "networkInterfaces":[
    {
      "accessConfigs":[
        {
          "name":"external-nat",
          "type":"ONE_TO_ONE_NAT"
        }
      ],
      "network":"https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/default"
    }
  ]
}'

O URI da imagem tem um ID de projeto diferente (debian-cloud) do ID do projeto, porque as imagens pertencem a diferentes projetos, dependendo do tipo de imagem. Por exemplo, todas as imagens do Debian disponíveis publicamente pelo Compute Engine são hospedadas no projeto debian-cloud.

Ao fazer referência a outro recurso, use o URI do recurso totalmente qualificado. Por exemplo, a propriedade network usa um URI totalmente qualificado para a rede default.

Exemplos de solicitações da API

Python

from __future__ import annotations

import re
import sys
from typing import Any
import warnings

from google.api_core.extended_operation import ExtendedOperation
from google.cloud import compute_v1


def get_image_from_family(project: str, family: str) -> compute_v1.Image:
    """
    Retrieve the newest image that is part of a given family in a project.

    Args:
        project: project ID or project number of the Cloud project you want to get image from.
        family: name of the image family you want to get image from.

    Returns:
        An Image object.
    """
    image_client = compute_v1.ImagesClient()
    # List of public operating system (OS) images: https://cloud.google.com/compute/docs/images/os-details
    newest_image = image_client.get_from_family(project=project, family=family)
    return newest_image


def disk_from_image(
    disk_type: str,
    disk_size_gb: int,
    boot: bool,
    source_image: str,
    auto_delete: bool = True,
) -> compute_v1.AttachedDisk:
    """
    Create an AttachedDisk object to be used in VM instance creation. Uses an image as the
    source for the new disk.

    Args:
         disk_type: the type of disk you want to create. This value uses the following format:
            "zones/{zone}/diskTypes/(pd-standard|pd-ssd|pd-balanced|pd-extreme)".
            For example: "zones/us-west3-b/diskTypes/pd-ssd"
        disk_size_gb: size of the new disk in gigabytes
        boot: boolean flag indicating whether this disk should be used as a boot disk of an instance
        source_image: source image to use when creating this disk. You must have read access to this disk. This can be one
            of the publicly available images or an image from one of your projects.
            This value uses the following format: "projects/{project_name}/global/images/{image_name}"
        auto_delete: boolean flag indicating whether this disk should be deleted with the VM that uses it

    Returns:
        AttachedDisk object configured to be created using the specified image.
    """
    boot_disk = compute_v1.AttachedDisk()
    initialize_params = compute_v1.AttachedDiskInitializeParams()
    initialize_params.source_image = source_image
    initialize_params.disk_size_gb = disk_size_gb
    initialize_params.disk_type = disk_type
    boot_disk.initialize_params = initialize_params
    # Remember to set auto_delete to True if you want the disk to be deleted when you delete
    # your VM instance.
    boot_disk.auto_delete = auto_delete
    boot_disk.boot = boot
    return boot_disk


def wait_for_extended_operation(
    operation: ExtendedOperation, verbose_name: str = "operation", timeout: int = 300
) -> Any:
    """
    Waits for the extended (long-running) operation to complete.

    If the operation is successful, it will return its result.
    If the operation ends with an error, an exception will be raised.
    If there were any warnings during the execution of the operation
    they will be printed to sys.stderr.

    Args:
        operation: a long-running operation you want to wait on.
        verbose_name: (optional) a more verbose name of the operation,
            used only during error and warning reporting.
        timeout: how long (in seconds) to wait for operation to finish.
            If None, wait indefinitely.

    Returns:
        Whatever the operation.result() returns.

    Raises:
        This method will raise the exception received from `operation.exception()`
        or RuntimeError if there is no exception set, but there is an `error_code`
        set for the `operation`.

        In case of an operation taking longer than `timeout` seconds to complete,
        a `concurrent.futures.TimeoutError` will be raised.
    """
    result = operation.result(timeout=timeout)

    if operation.error_code:
        print(
            f"Error during {verbose_name}: [Code: {operation.error_code}]: {operation.error_message}",
            file=sys.stderr,
            flush=True,
        )
        print(f"Operation ID: {operation.name}", file=sys.stderr, flush=True)
        raise operation.exception() or RuntimeError(operation.error_message)

    if operation.warnings:
        print(f"Warnings during {verbose_name}:\n", file=sys.stderr, flush=True)
        for warning in operation.warnings:
            print(f" - {warning.code}: {warning.message}", file=sys.stderr, flush=True)

    return result


def create_instance(
    project_id: str,
    zone: str,
    instance_name: str,
    disks: list[compute_v1.AttachedDisk],
    machine_type: str = "n1-standard-1",
    network_link: str = "global/networks/default",
    subnetwork_link: str = None,
    internal_ip: str = None,
    external_access: bool = False,
    external_ipv4: str = None,
    accelerators: list[compute_v1.AcceleratorConfig] = None,
    preemptible: bool = False,
    spot: bool = False,
    instance_termination_action: str = "STOP",
    custom_hostname: str = None,
    delete_protection: bool = False,
) -> compute_v1.Instance:
    """
    Send an instance creation request to the Compute Engine API and wait for it to complete.

    Args:
        project_id: project ID or project number of the Cloud project you want to use.
        zone: name of the zone to create the instance in. For example: "us-west3-b"
        instance_name: name of the new virtual machine (VM) instance.
        disks: a list of compute_v1.AttachedDisk objects describing the disks
            you want to attach to your new instance.
        machine_type: machine type of the VM being created. This value uses the
            following format: "zones/{zone}/machineTypes/{type_name}".
            For example: "zones/europe-west3-c/machineTypes/f1-micro"
        network_link: name of the network you want the new instance to use.
            For example: "global/networks/default" represents the network
            named "default", which is created automatically for each project.
        subnetwork_link: name of the subnetwork you want the new instance to use.
            This value uses the following format:
            "regions/{region}/subnetworks/{subnetwork_name}"
        internal_ip: internal IP address you want to assign to the new instance.
            By default, a free address from the pool of available internal IP addresses of
            used subnet will be used.
        external_access: boolean flag indicating if the instance should have an external IPv4
            address assigned.
        external_ipv4: external IPv4 address to be assigned to this instance. If you specify
            an external IP address, it must live in the same region as the zone of the instance.
            This setting requires `external_access` to be set to True to work.
        accelerators: a list of AcceleratorConfig objects describing the accelerators that will
            be attached to the new instance.
        preemptible: boolean value indicating if the new instance should be preemptible
            or not. Preemptible VMs have been deprecated and you should now use Spot VMs.
        spot: boolean value indicating if the new instance should be a Spot VM or not.
        instance_termination_action: What action should be taken once a Spot VM is terminated.
            Possible values: "STOP", "DELETE"
        custom_hostname: Custom hostname of the new VM instance.
            Custom hostnames must conform to RFC 1035 requirements for valid hostnames.
        delete_protection: boolean value indicating if the new virtual machine should be
            protected against deletion or not.
    Returns:
        Instance object.
    """
    instance_client = compute_v1.InstancesClient()

    # Use the network interface provided in the network_link argument.
    network_interface = compute_v1.NetworkInterface()
    network_interface.network = network_link
    if subnetwork_link:
        network_interface.subnetwork = subnetwork_link

    if internal_ip:
        network_interface.network_i_p = internal_ip

    if external_access:
        access = compute_v1.AccessConfig()
        access.type_ = compute_v1.AccessConfig.Type.ONE_TO_ONE_NAT.name
        access.name = "External NAT"
        access.network_tier = access.NetworkTier.PREMIUM.name
        if external_ipv4:
            access.nat_i_p = external_ipv4
        network_interface.access_configs = [access]

    # Collect information into the Instance object.
    instance = compute_v1.Instance()
    instance.network_interfaces = [network_interface]
    instance.name = instance_name
    instance.disks = disks
    if re.match(r"^zones/[a-z\d\-]+/machineTypes/[a-z\d\-]+$", machine_type):
        instance.machine_type = machine_type
    else:
        instance.machine_type = f"zones/{zone}/machineTypes/{machine_type}"

    instance.scheduling = compute_v1.Scheduling()
    if accelerators:
        instance.guest_accelerators = accelerators
        instance.scheduling.on_host_maintenance = (
            compute_v1.Scheduling.OnHostMaintenance.TERMINATE.name
        )

    if preemptible:
        # Set the preemptible setting
        warnings.warn(
            "Preemptible VMs are being replaced by Spot VMs.", DeprecationWarning
        )
        instance.scheduling = compute_v1.Scheduling()
        instance.scheduling.preemptible = True

    if spot:
        # Set the Spot VM setting
        instance.scheduling.provisioning_model = (
            compute_v1.Scheduling.ProvisioningModel.SPOT.name
        )
        instance.scheduling.instance_termination_action = instance_termination_action

    if custom_hostname is not None:
        # Set the custom hostname for the instance
        instance.hostname = custom_hostname

    if delete_protection:
        # Set the delete protection bit
        instance.deletion_protection = True

    # Prepare the request to insert an instance.
    request = compute_v1.InsertInstanceRequest()
    request.zone = zone
    request.project = project_id
    request.instance_resource = instance

    # Wait for the create operation to complete.
    print(f"Creating the {instance_name} instance in {zone}...")

    operation = instance_client.insert(request=request)

    wait_for_extended_operation(operation, "instance creation")

    print(f"Instance {instance_name} created.")
    return instance_client.get(project=project_id, zone=zone, instance=instance_name)

Java

public static Operation startInstance(Compute compute, String instanceName) throws IOException {
  System.out.println("================== Starting New Instance ==================");

  // Create VM Instance object with the required properties.
  Instance instance = new Instance();
  instance.setName(instanceName);
  instance.setMachineType(
      String.format(
          "https://www.googleapis.com/compute/v1/projects/%s/zones/%s/machineTypes/e2-standard-1",
          PROJECT_ID, ZONE_NAME));
  // Add Network Interface to be used by VM Instance.
  NetworkInterface ifc = new NetworkInterface();
  ifc.setNetwork(
      String.format(
          "https://www.googleapis.com/compute/v1/projects/%s/global/networks/default",
          PROJECT_ID));
  List<AccessConfig> configs = new ArrayList<>();
  AccessConfig config = new AccessConfig();
  config.setType(NETWORK_INTERFACE_CONFIG);
  config.setName(NETWORK_ACCESS_CONFIG);
  configs.add(config);
  ifc.setAccessConfigs(configs);
  instance.setNetworkInterfaces(Collections.singletonList(ifc));

  // Add attached Persistent Disk to be used by VM Instance.
  AttachedDisk disk = new AttachedDisk();
  disk.setBoot(true);
  disk.setAutoDelete(true);
  disk.setType("PERSISTENT");
  AttachedDiskInitializeParams params = new AttachedDiskInitializeParams();
  // Assign the Persistent Disk the same name as the VM Instance.
  params.setDiskName(instanceName);
  // Specify the source operating system machine image to be used by the VM Instance.
  params.setSourceImage(SOURCE_IMAGE_PREFIX + SOURCE_IMAGE_PATH);
  // Specify the disk type as Standard Persistent Disk
  params.setDiskType(
      String.format(
          "https://www.googleapis.com/compute/v1/projects/%s/zones/%s/diskTypes/pd-standard",
          PROJECT_ID, ZONE_NAME));
  disk.setInitializeParams(params);
  instance.setDisks(Collections.singletonList(disk));

  // Initialize the service account to be used by the VM Instance and set the API access scopes.
  ServiceAccount account = new ServiceAccount();
  account.setEmail("default");
  List<String> scopes = new ArrayList<>();
  scopes.add("https://www.googleapis.com/auth/devstorage.full_control");
  scopes.add("https://www.googleapis.com/auth/compute");
  account.setScopes(scopes);
  instance.setServiceAccounts(Collections.singletonList(account));

  // Optional - Add a startup script to be used by the VM Instance.
  Metadata meta = new Metadata();
  Metadata.Items item = new Metadata.Items();
  item.setKey("startup-script-url");
  // If you put a script called "vm-startup.sh" in this Google Cloud Storage
  // bucket, it will execute on VM startup.  This assumes you've created a
  // bucket named the same as your PROJECT_ID.
  // For info on creating buckets see:
  // https://cloud.google.com/storage/docs/cloud-console#_creatingbuckets
  item.setValue(String.format("gs://%s/vm-startup.sh", PROJECT_ID));
  meta.setItems(Collections.singletonList(item));
  instance.setMetadata(meta);

  System.out.println(instance.toPrettyString());
  Compute.Instances.Insert insert = compute.instances().insert(PROJECT_ID, ZONE_NAME, instance);
  return insert.execute();
}

Como criar URIs de recurso

Na API Compute Engine, uma referência a outro Google Cloud recurso é fornecida como um URI totalmente qualificado:

https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/RESOURCE_TYPE/SPECIFIC_RESOURCE

Ao usar a API, sempre que especificar uma imagem, um tipo de máquina, uma rede ou qualquer outro recurso, você precisará fornecer o URI dele. Ferramentas de cliente como a Google Cloud CLI e o Google Cloud console ocultam essa complexidade e lidam com a criação dos URIs de recursos para você, mas ao interagir com a API diretamente, você precisa criá-los por conta própria.

Há URIs de recurso visivelmente diferentes para tipos de recursos distintos. Por exemplo, um recurso zonal tem a especificação zone no URI:

https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/machineTypes/e2-standard-2

Os recursos regionais substituem a especificação zone por uma especificação region:

https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/regions/REGION/addresses/ADDRESS_NAME

Da mesma forma, os recursos globais têm a especificação global:

https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/global/images/VM_NAME

A API Compute Engine também aceita URIs parciais porque o serviço pode inferir informações como o ID do projeto. Portanto, as seguintes versões parciais dos URIs anteriores também são aceitáveis:

zones/ZONE/machineTypes/e2-standard-2
regions/REGION/addresses/ADDRESS_NAME
project/PROJECT_ID/global/images/VM_NAME

Nos URIs parciais, os URIs zonais e regionais omitiram o código do projeto, mas o URI da imagem não. Isso ocorre porque as imagens disponíveis publicamente oferecidas pelo Compute Engine são hospedadas em outros projetos, como debian-cloud para todas as imagens do Debian e ubuntu-os-cloud para todas as imagens do Ubuntu. Antes de usar essas imagens, você precisa fornecer o ID do projeto apropriado. Se você omitir o ID do projeto para imagens, o Compute Engine tentará encontrar a imagem no seu projeto, e a solicitação falhará porque a imagem não existe.

No entanto, se você usar uma imagem personalizada que pertence ao seu projeto (o mesmo em que está criando esse recurso), será possível omitir a especificação do projeto ao fornecer um URI de imagem.

Como determinar as propriedades necessárias

A documentação de referência da API Compute Engine, disponível para as APIs v1 e beta, descreve todas as propriedades possíveis que podem ser definidas para um recurso específico. Na documentação de referência, há uma distinção entre as propriedades mutáveis e as imutáveis, marcadas com [Output Only] na descrição da propriedade. Entretanto, para determinar as propriedades necessárias para um recurso, é necessário analisar a documentação específica a essa tarefa.

Por exemplo, se você estiver criando uma instância, leia a documentação Como criar uma instância a partir de uma imagem para ver as propriedades da API necessárias para a solicitação. Se você quiser criar um endereço IP externo estático na API, leia a documentação Endereços IP externos estáticos.

Como validar solicitações de API

Para validar suas solicitações de API, faça o seguinte:

  1. Na referência da API Compute Engine, encontre o método que seu código está chamando. Por exemplo, v1/compute.instances.insert.
  2. No menu de conteúdo, clique em Testar. Isso abre a janela Testar esta API.

    Faça um teste. no menu de conteúdo.

  3. Em Parâmetros de solicitação, não é preciso fornecer um projeto ou zona, porque a validação não exige o envio da solicitação.

  4. Em Corpo da solicitação, cole sua solicitação.

    A janela &quot;Testar esta API&quot;, exibindo o campo &quot;Corpo da solicitação&quot; para mostrar onde colar uma solicitação de validação.

Os elementos incorretos da solicitação estão sublinhados em azul. Clique em cada seção sublinhada para mais informações sobre o problema a ser resolvido.

Como manipular as respostas de API

Se você fizer uma solicitação que altere (altera) dados, o Compute Engine retornará um objeto Operation que pode ser pesquisado para receber o status das operações da solicitação. O recurso Operation tem esta aparência:

{
 "kind": "compute#operation",
 "id": "7127550864823803662",
 "name": "operation-1458856416788-52ed27a803e22-1c3bd86a-9e95017b",
 "zone": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE",
 "operationType": "insert",
 "targetLink": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/EXAMPLE_VM",
 "targetId": "4132355614508179214",
 "status": "PENDING",
 "user": "user@example.com",
 "progress": 0,
 "insertTime": "2016-03-24T14:53:37.788-07:00",
 "selfLink": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/operations/operation-1458856416788-52ed27a803e22-1c3bd86a-9e95017b"
}

Se a solicitação original for transformar (alterar) um recurso zonal, por exemplo, para capturar o snapshot de um disco ou interromper uma instância, o Compute Engine retornará um objeto zoneOperations. Da mesma forma, os recursos regionais e globais retornam um objeto regionOperations ou globalOperations, respectivamente. Para ver o status de uma operação, execute uma solicitação que use o método get ou wait no recurso específico Operation e forneça o name da operação.

Sua solicitação só está completa quando o status de recurso Operation é retornado como DONE. Isso pode levar algum tempo, dependendo da natureza da solicitação. Depois que o status do recurso Operation é retornado como DONE, verifique se a operação teve êxito e se houve algum erro. Isso pode levar algum tempo, dependendo da natureza da solicitação. Depois que o status do recurso Operation é retornado como DONE, verifique se a operação teve êxito e se houve algum erro.

Por exemplo, a resposta a seguir indica que a operação anterior foi concluída, especificada pelo status DONE:

endTime: '2016-03-24T14:54:07.119-07:00'
id: '7127550864823803662'
insertTime: '2016-03-24T14:53:37.788-07:00'
kind: compute#operation
name: operation-1458856416788-52ed27a803e22-1c3bd86a-9e95017b
operationType: insert
progress: 100
selfLink: https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/operations/operation-1458856416788-52ed27a803e22-1c3bd86a-9e95017b
startTime: '2016-03-24T14:53:38.397-07:00'
status: DONE
targetId: '4132355614508179214'
targetLink: https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/EXAMPLE_VM
user: user@example.com
zone: https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE

Para confirmar, faça uma solicitação get ao recurso para verificar se ele existe e está em execução. Exemplo:

GET /compute/v1/projects/PROJECT_ID/zones/ZONE/instances/EXAMPLE_VM

{
  "cpuPlatform": "Intel Haswell",
  "creationTimestamp": "2016-03-24T14:53:37.170-07:00",
  "disks": [
    ..[snip]..
  "selfLink": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/EXAMPLE_VM",
  "status": "RUNNING",
  "tags": {
    "fingerprint": "42WmSpB8rSM="
  },
  "zone": "https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE"
}

Como pesquisar operações

É possível escrever um código para pesquisar periodicamente a operação com uma solicitação get ou wait que é retornada quando o status da operação é DONE.

Com uma solicitação get, a operação é retornada imediatamente, independentemente do status dela. Você precisa pesquisar a operação periodicamente para saber quando ela for concluída.

Se você fizer uma solicitação wait, ela retornará quando a operação for DONE ou se a solicitação estiver se aproximando do prazo de dois minutos. É possível usar wait ou get para pesquisar suas operações, mas o método wait oferece determinados benefícios em relação ao get:

  • Você pode configurar seus clientes para pesquisar o status da operação com menos frequência, reduzindo o uso de QPS da API Compute Engine.
  • A latência média entre o momento em que a operação é concluída e quando o cliente é informado de que ela foi concluída é significativamente reduzida, porque o servidor responde assim que é concluído.
  • O método fornece esperas limitadas. O método aguarda no máximo o tempo limite HTTP padrão (2 minutos) e retorna o estado atual da operação, que pode ser DONE ou ainda em andamento.

O método wait é uma API de melhor esforço. Se o servidor estiver sobrecarregado, a solicitação poderá retornar antes de atingir o prazo padrão ou depois de aguardar apenas zero segundo. O método também não tem garantia de retornar somente quando a operação é DONE. Por exemplo, se a solicitação se aproximar do prazo de dois minutos, o método retornará mesmo que a operação não seja concluída. Para verificar suas operações, recomendamos usar o método wait ou get, em um loop de repetição com suspensão intermediária, para analisar periodicamente o status da operação. O intervalo máximo de tentativas não pode exceder o período mínimo de armazenamento da operação.

Exemplo de pesquisa

Os exemplos a seguir usam o método get. Você pode substituir o método get pelo método wait:

Python

from google.cloud import compute_v1


def wait_for_operation(
    operation: compute_v1.Operation, project_id: str
) -> compute_v1.Operation:
    """
    This method waits for an operation to be completed. Calling this function
    will block until the operation is finished.

    Args:
        operation: The Operation object representing the operation you want to
            wait on.
        project_id: project ID or project number of the Cloud project you want to use.

    Returns:
        Finished Operation object.
    """
    kwargs = {"project": project_id, "operation": operation.name}
    if operation.zone:
        client = compute_v1.ZoneOperationsClient()
        # Operation.zone is a full URL address of a zone, so we need to extract just the name
        kwargs["zone"] = operation.zone.rsplit("/", maxsplit=1)[1]
    elif operation.region:
        client = compute_v1.RegionOperationsClient()
        # Operation.region is a full URL address of a region, so we need to extract just the name
        kwargs["region"] = operation.region.rsplit("/", maxsplit=1)[1]
    else:
        client = compute_v1.GlobalOperationsClient()
    return client.wait(**kwargs)

Java

/**
 * Wait until {@code operation} is completed.
 *
 * @param compute the {@code Compute} object
 * @param operation the operation returned by the original request
 * @param timeout the timeout, in millis
 * @return the error, if any, else {@code null} if there was no error
 * @throws InterruptedException if we timed out waiting for the operation to complete
 * @throws IOException if we had trouble connecting
 */
public static Operation.Error blockUntilComplete(
    Compute compute, Operation operation, long timeout) throws Exception {
  long start = System.currentTimeMillis();
  final long pollInterval = 5 * 1000;
  String zone = getLastWordFromUrl(operation.getZone()); // null for global/regional operations
  String region = getLastWordFromUrl(operation.getRegion());
  String status = operation.getStatus();
  String opId = operation.getName();
  while (operation != null && !status.equals("DONE")) {
    Thread.sleep(pollInterval);
    long elapsed = System.currentTimeMillis() - start;
    if (elapsed >= timeout) {
      throw new InterruptedException("Timed out waiting for operation to complete");
    }
    System.out.println("waiting...");
    if (zone != null) {
      Compute.ZoneOperations.Get get = compute.zoneOperations().get(PROJECT_ID, zone, opId);
      operation = get.execute();
    } else if (region != null) {
      Compute.RegionOperations.Get get = compute.regionOperations().get(PROJECT_ID, region, opId);
      operation = get.execute();
    } else {
      Compute.GlobalOperations.Get get = compute.globalOperations().get(PROJECT_ID, opId);
      operation = get.execute();
    }
    if (operation != null) {
      status = operation.getStatus();
    }
  }
  return operation == null ? null : operation.getError();
}