创建 API 请求和处理响应

本文档描述如何构建 API 请求以及处理来自 Compute Engine API 的 API 响应。本文档介绍了如何执行以下操作:

  • 构建请求正文。
  • 确定请求所需的资源网址。
  • 处理 API 响应。
  • 确定 API 请求是否成功。

本文档未介绍如何向 API 进行身份验证。如需了解如何向 API 进行身份验证,请参阅向 Compute Engine 进行身份验证。

准备工作

创建 API 请求

Compute Engine API 要求 API 请求采用 JSON 格式。如需发出 API 请求,您可以使用 curl 或 httplib2 等工具发出直接 HTTP 请求,您也可以使用某个可用的客户端库。

当您发出需要请求正文的 API 请求(例如 POST、UPDATE 或 PATCH 请求)时,请求正文包含您要在此请求中设置的资源属性。例如,以下 curl 命令向实例资源 URI 发出 POST 请求。该请求会创建一个具有请求正文中定义的属性的实例。请求正文由 -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"
    }
  ]
}'

映像 URI 的项目 ID (debian-cloud) 与您的项目 ID 不同,这是因为映像属于不同的项目,具体取决于映像类型。例如,Compute Engine 提供的所有公开可用的 Debian 映像都托管在 debian-cloud 项目中。

引用其他资源时,请使用完全限定的资源 URI。例如,network 属性使用 default 网络的完全限定 URI。

API 请求示例

Python

def create_instance(
    compute: object,
    project: str,
    zone: str,
    name: str,
    bucket: str,
) -> str:
    """Creates an instance in the specified zone.

    Args:
      compute: an initialized compute service object.
      project: the Google Cloud project ID.
      zone: the name of the zone in which the instances should be created.
      name: the name of the instance.
      bucket: the name of the bucket in which the image should be written.

    Returns:
      The instance object.
    """
    # Get the latest Debian Jessie image.
    image_response = (
        compute.images()
        .getFromFamily(project="debian-cloud", family="debian-11")
        .execute()
    )
    source_disk_image = image_response["selfLink"]

    # Configure the machine
    machine_type = "zones/%s/machineTypes/n1-standard-1" % zone
    startup_script = open(
        os.path.join(os.path.dirname(__file__), "startup-script.sh")
    ).read()
    image_url = "http://storage.googleapis.com/gce-demo-input/photo.jpg"
    image_caption = "Ready for dessert?"

    config = {
        "name": name,
        "machineType": machine_type,
        # Specify the boot disk and the image to use as a source.
        "disks": [
            {
                "boot": True,
                "autoDelete": True,
                "initializeParams": {
                    "sourceImage": source_disk_image,
                },
            }
        ],
        # Specify a network interface with NAT to access the public
        # internet.
        "networkInterfaces": [
            {
                "network": "global/networks/default",
                "accessConfigs": [{"type": "ONE_TO_ONE_NAT", "name": "External NAT"}],
            }
        ],
        # Allow the instance to access cloud storage and logging.
        "serviceAccounts": [
            {
                "email": "default",
                "scopes": [
                    "https://www.googleapis.com/auth/devstorage.read_write",
                    "https://www.googleapis.com/auth/logging.write",
                ],
            }
        ],
        # Metadata is readable from the instance and allows you to
        # pass configuration from deployment scripts to instances.
        "metadata": {
            "items": [
                {
                    # Startup script is automatically executed by the
                    # instance upon startup.
                    "key": "startup-script",
                    "value": startup_script,
                },
                {"key": "url", "value": image_url},
                {"key": "text", "value": image_caption},
                {"key": "bucket", "value": bucket},
            ]
        },
    }

    return compute.instances().insert(project=project, zone=zone, body=config).execute()

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

创建资源 URI

在 Compute Engine API 中,对另一个 Google Cloud 资源的引用以完全限定的 URI 形式提供:

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

每次您指定映像、机器类型、网络或任何其他资源,都必须在使用 API 时提供资源的 URI。Google Cloud CLI 和 Google Cloud 控制台等客户端工具可隐藏这种复杂性并为您创建这些资源 URI,但是在与 API 直接交互时,您必须自行创建这些资源 URI。

不同类型资源的资源 URI 略有不同。例如,可用区级资源在 URI 中具有 zone 规范:

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

区域级资源将 zone 规范替换为 region 规范:

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

同样,全球性资源具有 global 规范:

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

Compute Engine API 也接受部分 URI,因为服务可以推断项目 ID 等信息。因此,早期 URI 的以下部分版本也是可以接受的:

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

在部分 URI 中,可用区 URI 和区域 URI 都省略了项目 ID,但映像 URI 未省略。这是因为 Compute Engine 提供的公开可用的映像托管在其他项目中,例如所有 Debian 映像都托管在 debian-cloud,所有 Ubuntu 映像都托管在 ubuntu-os-cloud。您需要提供适当的项目 ID 才能使用这些映像。如果您省略了映像的项目 ID,Compute Engine 会尝试在您的项目中查找该映像,由于映像不存在,请求会失败。

但是,如果您使用属于您的项目(您要在其中创建相应资源的项目)的自定义映像,则可以在提供映像 URI 时省略项目规范。

确定所需的属性

适用于 v1 和 Beta 版 API 的 Compute Engine API 参考文档介绍了您可以为特定资源设置的所有可能的属性。此参考文档区分了可变属性和不可变属性(在属性说明中用 [Output Only] 标记),但是如需确定资源所需的属性,您需要查看特定于该任务的文档。

例如,如果您要创建实例,请参阅从映像创建实例文档以查看请求所需的 API 属性。如果要在 API 中创建静态外部 IP 地址,请参阅静态外部 IP 地址文档。

验证 API 请求

如需验证您的 API 请求,请执行以下操作:

  1. 在 Compute Engine API 参考文档中,找到代码调用的方法。例如,v1/compute.instances.insert。
  2. 在内容菜单中,点击试试看!这会打开试用此 API 窗口。

    内容菜单上的“试试一下!”按钮。

  3. 在请求参数下,您无需提供项目或可用区,因为验证不需要提交请求。

  4. 在请求正文下,粘贴您的请求。

    “试用此 API”窗口,显示了“请求正文”字段,以显示粘贴请求位置进行验证。

请求中格式错误的元素带有蓝色下划线。点击每个带下划线的部分,详细了解要解决的问题。

处理 API 响应

如果您发出更改(改动)数据的请求,Compute Engine 会返回一个 Operation 对象,然后您可以轮询该对象以获取请求的操作状态。Operation 资源如下所示:

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

如果原始请求是更改可用区级资源(例如,截取磁盘快照或停止实例),则 Compute Engine 会返回 zoneOperations 对象。同样,区域级资源和全球性资源分别会返回 regionOperations 或 globalOperations 对象。如需获取操作的状态,您可以执行对特定 Operation 资源使用 get 或 wait 方法的请求并提供操作的 name。

直到 Operation 资源的状态返回 DONE 时,您的请求才会完成。该过程可能需要一些时间,具体取决于请求的性质。然后,在 Operation 资源的状态返回 DONE 之后,您可以查看操作是否成功以及是否存在任何错误。

例如,以下响应表明上述操作现已完成,由 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

若要确认这一点,请向资源发出 get 请求,以检查它是否存在且正在运行。例如:

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

轮询操作

您可以编写一些代码来使用 get 或 wait 请求定期轮询操作,当操作状态为 DONE 时这些操作会返回。

使用 get 请求时,无论操作的状态是什么,它会立即返回操作。您需要定期轮询操作,以了解操作何时完成。

如果您发出 wait 请求,则当操作为 DONE 或请求接近 2 分钟时限时,该请求将返回。您可以选择使用 wait 或 get 来轮询操作,但与 get 方法相比,wait 方法具有以下某些优势:

  • 您可以将客户端设置为以较低的频率轮询操作状态,从而减少 Compute Engine API 的 QPS 用量。
  • 操作完成与客户端收到操作完成的通知之间的平均延迟时间会显著缩短,因为服务器在操作完成后会立即做出响应。
  • 该方法的等待时间有界限。该方法等待的时间不会超过默认 HTTP 超时(2 分钟),然后返回操作的当前状态,该状态可能是 DONE 或仍在进行中。

wait 方法是尽力而为 API。如果服务器过载,则请求可能会在达到默认时限之前或等待零秒后返回。此方法也不保证仅在操作为 DONE 时返回。例如,如果请求接近 2 分钟时限,则即使操作尚未完成,该方法也会返回。如需检查操作,我们建议您使用 wait 或 get 方法(在中间存在休眠的重试循环中)定期轮询操作状态。重试间隔时间上限不应超过最短操作保留期限。

示例轮询

以下示例使用 get 方法。您可以将 get 方法替换为 wait 方法:

Python

def wait_for_operation(
    compute: object,
    project: str,
    zone: str,
    operation: str,
) -> dict:
    """Waits for the given operation to complete.

    Args:
      compute: an initialized compute service object.
      project: the Google Cloud project ID.
      zone: the name of the zone in which the operation should be executed.
      operation: the operation ID.

    Returns:
      The result of the operation.
    """
    print("Waiting for operation to finish...")
    while True:
        result = (
            compute.zoneOperations()
            .get(project=project, zone=zone, operation=operation)
            .execute()
        )

        if result["status"] == "DONE":
            print("done.")
            if "error" in result:
                raise Exception(result["error"])
            return result

        time.sleep(1)

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