Repository: getmoto/moto
Pull request #8125 (2024-09-15 11:33:02)

EC2: Ensure all regions have availability zones
Related to #8087 

Zones are now automatically created for every region that `botocore` returns - making it future proof for new regions added in the future.
Some explicit zone mappings were kept for backwards compatibility.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8125?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.49%. Comparing base [(`ca14870`)](https://app.codecov.io/gh/getmoto/moto/commit/ca148700bd020f20de923d5884ee0aeab667c17f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`fb83b3c`)](https://app.codecov.io/gh/getmoto/moto/commit/fb83b3c6a20839b268bbf2c3d8da3afdebbb5b24?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 2 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #8125   +/-   ##
=======================================
  Coverage   94.48%   94.49%           
=======================================
  Files        1150     1150           
  Lines       99011    99021   +10     
=======================================
+ Hits        93555    93567   +12     
+ Misses       5456     5454    -2     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8125/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8125/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.81% <100.00%> (+<0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8125/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.46% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8125?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/ec2/models/availability_zones_and_regions.py b/moto/ec2/models/availability_zones_and_regions.py
index 34475a87e..5191866d6 100644
--- a/moto/ec2/models/availability_zones_and_regions.py
+++ b/moto/ec2/models/availability_zones_and_regions.py
@@ -70,12 +70,16 @@ class RegionsAndZonesBackend:
             Region(region, f"ec2.{region}.amazonaws.com.cn", "opt-in-not-required")
         )
 
+    # Regions where the regions and zones are different from the norm
+    # Potential reasons:
+    #  - More or less zones than usual (!= 3)
+    #  - More zone id's then zones, resulting in a third zone with id '-az4'
+    #  - Unusual mapping, resulting in zone id's that are not numerically ordered, i.e. a=1, b=3, c=2
+    #
+    # The exact mapping is different from account to account
+    # The mapping defined here will be an amalgamation of different contributors/accounts
+    # We're keeping it as is for legacy/compatibility reasons
     zones = {
-        "af-south-1": [
-            Zone(region_name="af-south-1", name="af-south-1a", zone_id="afs1-az1"),
-            Zone(region_name="af-south-1", name="af-south-1b", zone_id="afs1-az2"),
-            Zone(region_name="af-south-1", name="af-south-1c", zone_id="afs1-az3"),
-        ],
         "ap-south-1": [
             Zone(region_name="ap-south-1", name="ap-south-1a", zone_id="aps1-az1"),
             Zone(region_name="ap-south-1", name="ap-south-1b", zone_id="aps1-az3"),
@@ -86,11 +90,6 @@ class RegionsAndZonesBackend:
             Zone(region_name="eu-west-3", name="eu-west-3b", zone_id="euw3-az2"),
             Zone(region_name="eu-west-3", name="eu-west-3c", zone_id="euw3-az3"),
         ],
-        "eu-north-1": [
-            Zone(region_name="eu-north-1", name="eu-north-1a", zone_id="eun1-az1"),
-            Zone(region_name="eu-north-1", name="eu-north-1b", zone_id="eun1-az2"),
-            Zone(region_name="eu-north-1", name="eu-north-1c", zone_id="eun1-az3"),
-        ],
         "eu-west-2": [
             Zone(region_name="eu-west-2", name="eu-west-2a", zone_id="euw2-az2"),
             Zone(region_name="eu-west-2", name="eu-west-2b", zone_id="euw2-az3"),
@@ -101,23 +100,6 @@ class RegionsAndZonesBackend:
             Zone(region_name="eu-west-1", name="eu-west-1b", zone_id="euw1-az1"),
             Zone(region_name="eu-west-1", name="eu-west-1c", zone_id="euw1-az2"),
         ],
-        "ap-northeast-3": [
-            Zone(
-                region_name="ap-northeast-3",
-                name="ap-northeast-3a",
-                zone_id="apne3-az1",
-            ),
-            Zone(
-                region_name="ap-northeast-3",
-                name="ap-northeast-3b",
-                zone_id="apne3-az2",
-            ),
-            Zone(
-                region_name="ap-northeast-3",
-                name="ap-northeast-3c",
-                zone_id="apne3-az3",
-            ),
-        ],
         "ap-northeast-2": [
             Zone(
                 region_name="ap-northeast-2",
@@ -157,38 +139,11 @@ class RegionsAndZonesBackend:
                 zone_id="apne1-az2",
             ),
         ],
-        "ap-east-1": [
-            Zone(region_name="ap-east-1", name="ap-east-1a", zone_id="ape1-az1"),
-            Zone(region_name="ap-east-1", name="ap-east-1b", zone_id="ape1-az2"),
-            Zone(region_name="ap-east-1", name="ap-east-1c", zone_id="ape1-az3"),
-        ],
-        "sa-east-1": [
-            Zone(region_name="sa-east-1", name="sa-east-1a", zone_id="sae1-az1"),
-            Zone(region_name="sa-east-1", name="sa-east-1b", zone_id="sae1-az2"),
-            Zone(region_name="sa-east-1", name="sa-east-1c", zone_id="sae1-az3"),
-        ],
         "ca-central-1": [
             Zone(region_name="ca-central-1", name="ca-central-1a", zone_id="cac1-az1"),
             Zone(region_name="ca-central-1", name="ca-central-1b", zone_id="cac1-az2"),
             Zone(region_name="ca-central-1", name="ca-central-1d", zone_id="cac1-az4"),
         ],
-        "ap-southeast-1": [
-            Zone(
-                region_name="ap-southeast-1",
-                name="ap-southeast-1a",
-                zone_id="apse1-az1",
-            ),
-            Zone(
-                region_name="ap-southeast-1",
-                name="ap-southeast-1b",
-                zone_id="apse1-az2",
-            ),
-            Zone(
-                region_name="ap-southeast-1",
-                name="ap-southeast-1c",
-                zone_id="apse1-az3",
-            ),
-        ],
         "ap-southeast-2": [
             Zone(
                 region_name="ap-southeast-2",
@@ -206,38 +161,6 @@ class RegionsAndZonesBackend:
                 zone_id="apse2-az2",
             ),
         ],
-        "ap-southeast-3": [
-            Zone(
-                region_name="ap-southeast-3",
-                name="ap-southeast-3a",
-                zone_id="apse3-az1",
-            ),
-            Zone(
-                region_name="ap-southeast-3",
-                name="ap-southeast-3b",
-                zone_id="apse3-az2",
-            ),
-            Zone(
-                region_name="ap-southeast-3",
-                name="ap-southeast-3c",
-                zone_id="apse3-az3",
-            ),
-        ],
-        "eu-central-1": [
-            Zone(region_name="eu-central-1", name="eu-central-1a", zone_id="euc1-az2"),
-            Zone(region_name="eu-central-1", name="eu-central-1b", zone_id="euc1-az3"),
-            Zone(region_name="eu-central-1", name="eu-central-1c", zone_id="euc1-az1"),
-        ],
-        "eu-central-2": [
-            Zone(region_name="eu-central-2", name="eu-central-2a", zone_id="euc2-az1"),
-            Zone(region_name="eu-central-2", name="eu-central-2b", zone_id="euc2-az2"),
-            Zone(region_name="eu-central-2", name="eu-central-2c", zone_id="euc2-az3"),
-        ],
-        "eu-south-1": [
-            Zone(region_name="eu-south-1", name="eu-south-1a", zone_id="eus1-az1"),
-            Zone(region_name="eu-south-1", name="eu-south-1b", zone_id="eus1-az2"),
-            Zone(region_name="eu-south-1", name="eu-south-1c", zone_id="eus1-az3"),
-        ],
         "us-east-1": [
             Zone(region_name="us-east-1", name="us-east-1a", zone_id="use1-az6"),
             Zone(region_name="us-east-1", name="us-east-1b", zone_id="use1-az1"),
@@ -246,11 +169,6 @@ class RegionsAndZonesBackend:
             Zone(region_name="us-east-1", name="us-east-1e", zone_id="use1-az3"),
             Zone(region_name="us-east-1", name="us-east-1f", zone_id="use1-az5"),
         ],
-        "us-east-2": [
-            Zone(region_name="us-east-2", name="us-east-2a", zone_id="use2-az1"),
-            Zone(region_name="us-east-2", name="us-east-2b", zone_id="use2-az2"),
-            Zone(region_name="us-east-2", name="us-east-2c", zone_id="use2-az3"),
-        ],
         "us-west-1": [
             Zone(region_name="us-west-1", name="us-west-1a", zone_id="usw1-az3"),
             Zone(region_name="us-west-1", name="us-west-1b", zone_id="usw1-az1"),
@@ -261,55 +179,27 @@ class RegionsAndZonesBackend:
             Zone(region_name="us-west-2", name="us-west-2c", zone_id="usw2-az3"),
             Zone(region_name="us-west-2", name="us-west-2d", zone_id="usw2-az4"),
         ],
-        "me-south-1": [
-            Zone(region_name="me-south-1", name="me-south-1a", zone_id="mes1-az1"),
-            Zone(region_name="me-south-1", name="me-south-1b", zone_id="mes1-az2"),
-            Zone(region_name="me-south-1", name="me-south-1c", zone_id="mes1-az3"),
-        ],
         "cn-north-1": [
             Zone(region_name="cn-north-1", name="cn-north-1a", zone_id="cnn1-az1"),
             Zone(region_name="cn-north-1", name="cn-north-1b", zone_id="cnn1-az2"),
         ],
-        "cn-northwest-1": [
-            Zone(
-                region_name="cn-northwest-1",
-                name="cn-northwest-1a",
-                zone_id="cnnw1-az1",
-            ),
-            Zone(
-                region_name="cn-northwest-1",
-                name="cn-northwest-1b",
-                zone_id="cnnw1-az2",
-            ),
-            Zone(
-                region_name="cn-northwest-1",
-                name="cn-northwest-1c",
-                zone_id="cnnw1-az3",
-            ),
-        ],
-        "us-gov-west-1": [
-            Zone(
-                region_name="us-gov-west-1", name="us-gov-west-1a", zone_id="usgw1-az1"
-            ),
-            Zone(
-                region_name="us-gov-west-1", name="us-gov-west-1b", zone_id="usgw1-az2"
-            ),
-            Zone(
-                region_name="us-gov-west-1", name="us-gov-west-1c", zone_id="usgw1-az3"
-            ),
-        ],
-        "us-gov-east-1": [
-            Zone(
-                region_name="us-gov-east-1", name="us-gov-east-1a", zone_id="usge1-az1"
-            ),
-            Zone(
-                region_name="us-gov-east-1", name="us-gov-east-1b", zone_id="usge1-az2"
-            ),
-            Zone(
-                region_name="us-gov-east-1", name="us-gov-east-1c", zone_id="usge1-az3"
-            ),
-        ],
     }
+    # Regularized region->zone mapping for all regions not defined above
+    for region in regions:
+        if region.name not in zones:
+            name = region.name
+            region_parts = name.split("-")
+            shorthand = "usg" if name.startswith("us-gov") else region_parts[0]
+            # North and south first - this also handles combinations like northeast -> ne
+            for cardinal in ["north", "south", "central", "east", "west"]:
+                if cardinal in name:
+                    shorthand += cardinal[0]
+            shorthand += region_parts[-1]
+            zones[region.name] = [
+                Zone(region_name=name, name=f"{name}a", zone_id=f"{shorthand}-az1"),
+                Zone(region_name=name, name=f"{name}b", zone_id=f"{shorthand}-az2"),
+                Zone(region_name=name, name=f"{name}c", zone_id=f"{shorthand}-az3"),
+            ]
 
     def describe_regions(
         self, region_names: Optional[List[str]] = None


Test patch:
diff --git a/tests/test_ec2/test_availability_zones_and_regions.py b/tests/test_ec2/test_availability_zones_and_regions.py
index 16874457a..d092bcde4 100644
--- a/tests/test_ec2/test_availability_zones_and_regions.py
+++ b/tests/test_ec2/test_availability_zones_and_regions.py
@@ -6,7 +6,7 @@ from moto import mock_aws
 
 
 @mock_aws
-def test_boto3_describe_regions():
+def test_describe_regions():
     ec2 = boto3.client("ec2", "us-east-1")
     resp = ec2.describe_regions()
     assert len(resp["Regions"]) > 1
@@ -27,14 +27,15 @@ def test_boto3_describe_regions():
 
 
 @mock_aws
-def test_boto3_availability_zones():
+def test_availability_zones():
     ec2 = boto3.client("ec2", "us-east-1")
     resp = ec2.describe_regions()
     regions = [r["RegionName"] for r in resp["Regions"]]
     for region in regions:
         conn = boto3.client("ec2", region)
-        resp = conn.describe_availability_zones()
-        for rec in resp["AvailabilityZones"]:
+        zones = conn.describe_availability_zones()["AvailabilityZones"]
+        assert len(zones) > 0, f"Region {region} has no availability zones configured"
+        for rec in zones:
             assert region in rec["ZoneName"]
 
 
@@ -92,12 +93,27 @@ def test_describe_availability_zones_dryrun():
 
 
 @mock_aws
-def test_boto3_zoneId_in_availability_zones():
-    conn = boto3.client("ec2", "us-east-1")
+@pytest.mark.parametrize(
+    "region,shorthand",
+    [
+        # Regular regions
+        ("eu-central-1", "euc1-az"),
+        ("sa-east-1", "sae1-az"),
+        ("us-east-1", "use1-az"),
+        ("us-west-1", "usw1-az"),
+        # Regions never explicitly specified
+        ("ap-southeast-5", "apse5-az"),
+        # Regions with multiple cardinal points
+        ("ap-northeast-1", "apne1-az"),
+        ("ap-southeast-3", "apse3-az"),
+        # Edge cases with different partitions
+        ("cn-northwest-1", "cnnw1-az"),
+        ("us-gov-east-1", "usge1-az"),
+        ("us-gov-west-1", "usgw1-az"),
+    ],
+)
+def test_zoneId_in_availability_zones(region, shorthand):
+    conn = boto3.client("ec2", region)
     resp = conn.describe_availability_zones()
     for rec in resp["AvailabilityZones"]:
-        assert "use1" in rec.get("ZoneId")
-    conn = boto3.client("ec2", "us-west-1")
-    resp = conn.describe_availability_zones()
-    for rec in resp["AvailabilityZones"]:
-        assert "usw1" in rec.get("ZoneId")
+        assert shorthand in rec.get("ZoneId")