Créer des requêtes API et gérer les réponses

Ce document explique comment créer des requêtes API et gérer les réponses de l'API Compute Engine. Il décrit comment effectuer les tâches suivantes :

  • Créer le corps d'une requête
  • Identifier les URI de ressources nécessaires pour une requête
  • Gérer les réponses de l'API
  • Déterminer si une requête API a abouti

Ce document n'explique pas comment s'authentifier auprès de l'API. Pour savoir comment gérer l'authentification auprès de l'API, consultez la page S'authentifier sur Compute Engine.

Avant de commencer

  • Familiarisez-vous avec les API REST.
  • Découvrez comment vous authentifier auprès de l'API Compute Engine.

Créer une requête API

L'API Compute Engine requiert que les requêtes API soient au format JSON. Pour effectuer une requête API, vous pouvez effectuer une requête HTTP directe à l'aide d'outils tels que curl ou httplib2, ou utiliser l'une des bibliothèques clientes disponibles.

Lorsque vous effectuez une requête API nécessitant un corps de requête, telle que POST, UPDATE ou PATCH, le corps de la requête contient les propriétés de ressource que vous souhaitez définir dans requête. Par exemple, la commande curl suivante permet d'envoyer une requête POST à l'URI de ressource d'instances. La requête crée une instance avec les propriétés définies dans le corps de la requête. Le corps de la requête est indiqué par l'option -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"
    }
  ]
}'

L'URI d'image possède un ID de projet différent (debian-cloud) de votre ID de projet, car les images appartiennent à différents projets selon leur type. Par exemple, toutes les images Debian publiques fournies par Compute Engine sont hébergées dans le projet debian-cloud.

Lorsque vous référencez une autre ressource, utilisez l'URI complet de la ressource. Par exemple, la propriété network utilise un URI complet du réseau default.

Exemples de requêtes 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();
}

Créer des URI de ressources

Dans l'API Compute Engine, une référence à une autre ressource Google Cloud est attribuée en tant qu'URI complet :

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

Chaque fois que vous spécifiez une image, un type de machine, un réseau ou toute autre ressource, vous devez fournir l'URI de la ressource lorsque vous utilisez l'API. Les outils client tels que la Google Cloud CLI et la console Google Cloud masquent cette complexité et gèrent la création de ces URI de ressources pour vous. Toutefois, lorsque vous interagissez directement avec l'API, vous devez créer vous-même ces URI de ressources.

Les URI de ressources varient légèrement en fonction des types de ressources. Par exemple, l'URI d'une ressource zonale inclut la spécification zone :

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

Les ressources régionales remplacent la spécification zone par une spécification region :

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

De même, les ressources globales ont la spécification global :

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

L'API Compute Engine accepte également des URI partiels, car le service peut déduire des informations telles que l'ID de projet. Par conséquent, les versions partielles suivantes des URI précédents sont également acceptables :

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

Dans les URI partiels ci-dessus, l'ID de projet est omis pour les URI des ressources zonale et régionale, mais pas pour l'URI d'image. En effet, les images publiques proposées par Compute Engine sont hébergées dans d'autres projets, tels que debian-cloud pour toutes les images Debian et ubuntu-os-cloud pour toutes les images Ubuntu. Avant de pouvoir utiliser ces images, vous devez fournir l'ID de projet approprié. Si vous omettez l'ID du projet pour les images, Compute Engine tente de trouver l'image dans votre projet. Étant donné qu'elle n'existe pas, la requête échoue.

Toutefois, si vous utilisez une image personnalisée appartenant à votre projet (le projet dans lequel vous créez la ressource), vous pouvez omettre la spécification de projet lorsque vous indiquez un URI d'image.

Identifier les propriétés requises

La documentation de référence de l'API Compute Engine, disponible pour les API v1 et bêta, décrit toutes les propriétés que vous pouvez définir pour une ressource spécifique. Elle distingue les propriétés modifiables de celles qui sont non modifiables (indiqué par la mention [Output Only] dans la description des propriétés). Toutefois, pour déterminer les propriétés requises pour une ressource, vous devez examiner la documentation propre à la tâche concernée.

Par exemple, si vous créez une instance, consultez la documentation Créer une instance à partir d'une image pour afficher les propriétés d'API requises pour la requête. Si vous souhaitez créer une adresse IP externe statique dans l'API, consultez la documentation Adresses IP externes statiques.

Valider des requêtes API

Pour valider vos requêtes API, procédez comme suit :

  1. Dans la documentation de référence de l'API Compute Engine, recherchez la méthode appelée par votre code. Par exemple, v1/compute.instances.insert.
  2. Dans le menu "Contenu", cliquez sur Essayer. La fenêtre Essayer cette API s'ouvre.

    Bouton &quot;Essayer&quot; dans le menu &quot;Contenu&quot;

  3. Sous Paramètres de requête, vous n'avez pas besoin de spécifier de projet ni de zone, car la validation ne nécessite pas d'envoyer la requête.

  4. Sous Corps de la requête, collez votre requête.

    Fenêtre &quot;Essayer cette API&quot; affichant le champ &quot;Corps de la requête&quot; pour indiquer où coller une requête de validation

Les éléments non valides de la requête sont soulignés en bleu. Cliquez sur chaque section soulignée pour en savoir plus sur le problème à résoudre.

Gérer les réponses de l'API

Si vous effectuez une requête qui modifie des données, Compute Engine renvoie un objet Operation que vous pouvez interroger pour obtenir l'état des opérations de votre requête. La ressource Operation ressemble à ceci :

{
 "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"
}

Si la requête d'origine consiste à modifier une ressource zonale (par exemple, pour créer un instantané d'un disque ou pour arrêter une instance), Compute Engine renvoie un objet zoneOperations. De même, les ressources régionales et globales renvoient respectivement un objet regionOperations ou globalOperations. Vous pouvez obtenir l'état d'une opération en effectuant une requête qui utilise la méthode get ou wait pour la ressource Operation spécifique, en fournissant le name de l'opération.

Votre requête n'est pas terminée tant que l'état renvoyé pour la ressource Operation n'est pas DONE. Cela peut prendre un certain temps selon la nature de la requête. Dès que l'état DONE de la ressource Operation est renvoyé, vous devez vérifier si l'opération a abouti et si des erreurs se sont produites.

Par exemple, la réponse suivante indique que l'opération précédente est maintenant terminée, spécifiée par l'état 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

Pour confirmer, envoyez une requête get à la ressource pour vérifier qu'elle existe et qu'elle est en cours d'exécution. Par exemple :

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"
}

Interroger les opérations

Vous pouvez écrire du code pour interroger périodiquement l'opération avec une requête get ou wait qui renvoie l'état DONE.

Avec une requête get, l'opération est renvoyée immédiatement, quel que soit son état. Vous devez interroger périodiquement l'opération pour savoir quand elle est terminée.

Si vous envoyez une requête wait, la requête renvoie une réponse lorsque l'opération passe à l'état DONE ou lorsque le délai de deux minutes approche. Vous pouvez choisir d'utiliser wait ou get pour interroger vos opérations, mais la méthode wait offre certains avantages par rapport à la méthode get :

  • Vous pouvez configurer vos clients pour interroger l'état de l'opération moins fréquemment, ce qui réduit votre utilisation des RPS de l'API Compute Engine.
  • La latence moyenne entre la fin de l'opération et le moment où le client est informé que l'opération est terminée est considérablement réduite, car le serveur répond dès que l'opération est terminée.
  • Cette méthode fournit des temps d'attente limités. La méthode n'attend pas plus que le délai d'expiration HTTP par défaut (2 minutes), puis renvoie l'état actuel de l'opération, qui peut être DONE ou toujours en cours.

La méthode wait est une API best-effort (optimisée au mieux). Si le serveur est surchargé, la requête peut envoyer une réponse avant l'expiration du délai par défaut ou après seulement quelques secondes. La méthode ne garantit pas non plus qu'une réponse ne soit renvoyée que lorsque l'opération est à l'état DONE. Par exemple, si la requête approche du délai de 2 minutes, la méthode renvoie une réponse même si l'opération n'est pas terminée. Pour vérifier vos opérations, nous vous recommandons d'utiliser la méthode wait ou get (dans une boucle de nouvelle tentative avec veille intermédiaire) pour interroger régulièrement l'état de l'opération. L'intervalle maximal de nouvelle tentative ne doit pas dépasser la durée de conservation minimale des opérations.

Exemple d'interrogation

Les exemples suivants utilisent la méthode get. Vous pouvez remplacer la méthode get par la méthode wait :

Python

import time

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()

    while True:
        result = client.get(**kwargs)

        if result.status == compute_v1.Operation.Status.DONE:
            print("Operation finished.")
            if result.error:
                print(f"Error during operation: {result.error}")
            return result

        print("Waiting for operation to complete...")
        time.sleep(2)

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();
}