Check the zone has enough cpus and disks for remote instance creation.

- Only apply to cuttlefish avd type.

Bug: 147047953
Test: acloud-dev create
acloud-dev create_cf --build_target aosp_cf_x86_phone-userdebug --branch aosp-master \
--build_id 6124676 --autoconnect --kernel_build_target cf_x86_phone-userdebug
acloud-dev create --avd-type goldfish --build-target sdk_gphone_x86_64-userdebug \
--branch git_master
acloud-dev create --avd-type cheeps --branch git_qt-arc-dev --build-id 5539267 \
--build-target cheets_x86-userdebug
Change-Id: I05a236b3c3b422f181a2e5528e0e8d8d0b3deb92
diff --git a/errors.py b/errors.py
index 27e7505..6dcd83e 100644
--- a/errors.py
+++ b/errors.py
@@ -143,6 +143,10 @@
     """Path does not exist."""
 
 
+class CheckGCEZonesQuotaError(CreateError):
+    """There is no zone have enough quota."""
+
+
 class UnsupportedInstanceImageType(CreateError):
     """Unsupported create action for given instance/image type."""
 
diff --git a/internal/lib/android_compute_client.py b/internal/lib/android_compute_client.py
index 1bc69bc..71baab1 100755
--- a/internal/lib/android_compute_client.py
+++ b/internal/lib/android_compute_client.py
@@ -82,6 +82,21 @@
         self._instance_name_pattern = acloud_config.instance_name_pattern
         self._AddPerInstanceSshkey()
 
+    # TODO(147047953): New args to contorl zone metrics check.
+    def _VerifyZoneByQuota(self):
+        """Verify the zone must have enough quota to create instance.
+
+        Returns:
+            Boolean, True if zone have enough quota to create instance.
+
+        Raises:
+            errors.CheckGCEZonesQuotaError: the zone doesn't have enough quota.
+        """
+        if self.EnoughMetricsInZone(self._zone):
+            return True
+        raise errors.CheckGCEZonesQuotaError(
+            "There is no enough quota in zone: %s" % self._zone)
+
     def _AddPerInstanceSshkey(self):
         """Add per-instance ssh key.
 
@@ -396,21 +411,3 @@
         """
         return super(AndroidComputeClient, self).GetSerialPortOutput(
             instance, zone or self._zone, port)
-
-    def GetInstanceNamesByIPs(self, ips, zone=None):
-        """Get Instance names by IPs.
-
-        This function will go through all instances, which
-        could be slow if there are too many instances.  However, currently
-        GCE doesn't support search for instance by IP.
-
-        Args:
-            ips: A set of IPs.
-            zone: String, representing zone name, e.g. "us-central1-f"
-
-        Returns:
-            A dictionary where key is ip and value is instance name or None
-            if instance is not found for the given IP.
-        """
-        return super(AndroidComputeClient, self).GetInstanceNamesByIPs(
-            ips, zone or self._zone)
diff --git a/internal/lib/cvd_compute_client_multi_stage.py b/internal/lib/cvd_compute_client_multi_stage.py
index 2c1821c..05bcef9 100644
--- a/internal/lib/cvd_compute_client_multi_stage.py
+++ b/internal/lib/cvd_compute_client_multi_stage.py
@@ -188,6 +188,7 @@
         if avd_spec and avd_spec.instance_name_to_reuse:
             self._ip = self._ReusingGceInstance(avd_spec)
         else:
+            self._VerifyZoneByQuota()
             self._ip = self._CreateGceInstance(instance, image_name, image_project,
                                                extra_scopes, boot_disk_size_gb,
                                                avd_spec)
diff --git a/internal/lib/cvd_compute_client_multi_stage_test.py b/internal/lib/cvd_compute_client_multi_stage_test.py
index c0fed24..2ee543a 100644
--- a/internal/lib/cvd_compute_client_multi_stage_test.py
+++ b/internal/lib/cvd_compute_client_multi_stage_test.py
@@ -83,6 +83,8 @@
         """Set up the test."""
         super(CvdComputeClientTest, self).setUp()
         self.Patch(cvd_compute_client_multi_stage.CvdComputeClient, "InitResourceHandle")
+        self.Patch(cvd_compute_client_multi_stage.CvdComputeClient, "_VerifyZoneByQuota",
+                   return_value=True)
         self.Patch(android_build_client.AndroidBuildClient, "InitResourceHandle")
         self.Patch(android_build_client.AndroidBuildClient, "DownloadArtifact")
         self.Patch(list_instances, "GetInstancesFromInstanceNames", return_value=mock.MagicMock())
diff --git a/internal/lib/gcompute_client.py b/internal/lib/gcompute_client.py
index 3c0b295..2a1d414 100755
--- a/internal/lib/gcompute_client.py
+++ b/internal/lib/gcompute_client.py
@@ -51,6 +51,14 @@
 _ITEMS = "items"
 _METADATA = "metadata"
 _ZONE_RE = re.compile(r"^zones/(?P<zone>.+)")
+# Quota metrics
+_METRIC_CPUS = "CPUS"
+_METRIC_DISKS_GB = "DISKS_TOTAL_GB"
+_METRICS = [_METRIC_CPUS, _METRIC_DISKS_GB]
+_USAGE = "usage"
+_LIMIT = "limit"
+# The minimum requirement to create an instance.
+_REQUIRE_METRICS = {_METRIC_CPUS: 8, _METRIC_DISKS_GB: 1000}
 
 BASE_DISK_ARGS = {
     "type": "PERSISTENT",
@@ -196,11 +204,75 @@
         """Get project information.
 
         Returns:
-          A project resource in json.
+            A project resource in json.
         """
         api = self.service.projects().get(project=self._project)
         return self.Execute(api)
 
+    def GetRegionInfo(self):
+        """Get region information that includes all quotas limit.
+
+        The region info example:
+        {"items":
+            [{"status": "UP",
+              "name": "asia-east1",
+              "quotas":
+                [{"usage": 92, "metric": "CPUS", "limit": 100},
+                 {"usage": 640, "metric": "DISKS_TOTAL_GB", "limit": 10240},
+              ...]]}
+        }
+
+        Returns:
+            A region resource in json.
+        """
+        api = self.service.regions().list(project=self._project)
+        return self.Execute(api)
+
+    @staticmethod
+    def GetMetricQuota(regions_info, zone, metric):
+        """Get CPU quota limit in specific zone and project.
+
+        Args:
+            regions_info: Dict, regions resource in json.
+            zone: String, name of zone.
+            metric: String, name of metric, e.g. "CPUS".
+
+        Returns:
+            A dict of quota information. Such as
+            {"usage": 100, "metric": "CPUS", "limit": 200}
+        """
+        for region_info in regions_info["items"]:
+            if region_info["name"] in zone:
+                for quota in region_info["quotas"]:
+                    if quota["metric"] == metric:
+                        return quota
+        logger.info("Can't get %s quota info from zone(%s)", metric, zone)
+        return None
+
+    def EnoughMetricsInZone(self, zone):
+        """Check the zone have enough metrics to create instance.
+
+        The metrics include CPUS and DISKS.
+
+        Args:
+            zone: String, name of zone.
+
+        Returns:
+            Boolean. True if zone have enough quota.
+        """
+        regions_info = self.GetRegionInfo()
+        for metric in _METRICS:
+            quota = self.GetMetricQuota(regions_info, zone, metric)
+            if not quota:
+                logger.debug(
+                    "Can't query the metric(%s) in zone(%s)", metric, zone)
+                return False
+            if quota[_LIMIT] - quota[_USAGE] < _REQUIRE_METRICS[metric]:
+                logger.debug(
+                    "The metric(%s) is over limit in zone(%s)", metric, zone)
+                return False
+        return True
+
     def GetDisk(self, disk_name, zone):
         """Get disk information.