Skip to content

Commit 92b0283

Browse files
authored
{Zones} Add new az zones extension module (#8704)
1 parent a078070 commit 92b0283

96 files changed

Lines changed: 7835 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,6 @@
324324

325325
/src/azext_durabletask/ @RyanLettieri
326326

327-
/src/acat @qinqingxu @Sherylueen @yongxin-ms @wh-alice
327+
/src/acat @qinqingxu @Sherylueen @yongxin-ms @wh-alice
328+
329+
/src/zones/ @nielsams

src/service_name.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,11 @@
919919
"AzureServiceName": "Microsoft Connected Cache",
920920
"URL": ""
921921
},
922+
{
923+
"Command": "az zones",
924+
"AzureServiceName": "Availability Zones",
925+
"URL": "https://learn.microsoft.com/azure/availability-zones/az-overview"
926+
},
922927
{
923928
"Command": "az playwright-testing",
924929
"AzureServiceName": "Playwright Testing",

src/zones/HISTORY.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. :changelog:
2+
3+
Release History
4+
===============
5+
6+
1.0.0b1
7+
++++++
8+
* Initial preview release.

src/zones/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Microsoft Azure CLI 'zones' Extension
2+
3+
This package is for the 'zones' extension.
4+
i.e. 'az zones'
5+
6+
This CLI Extension helps validate the zone redundancy status of resources within a specific scope.
7+
For each resource, one of the following statuses will be returned:
8+
Unknown # Unable to verify status. You'll need to check the resource manually.
9+
Yes # Resource is configured for zone redundancy
10+
Always # Resource is always zone redundant, no configuration needed
11+
No # Resource is not configured for zone redundancy, but could be in another configuration
12+
Never # Resource cannot be configured for zone redundancy
13+
Dependent # Resource is zone redundant if parent or related resource is zone redundant
14+
NoZonesInRegion # The region the resource is deployed in does not have Availability Zones
15+
16+
> [!NOTE]
17+
> This extension is in active development. While an effort has been made to include the most common resource types and their zone redundancy configuration, there are still plenty of resource types missing. More will be added in future releases. In the meantime, if you need specific resources added or have found errors, please raise a Github issue.
18+
19+
## When should you use this?
20+
21+
In order to build a fully zone redundant application, you need to satisfy three criteria:
22+
23+
1) Enable zone redundancy on all PaaS resources in the application
24+
2) Ensure zonal resources are spread across all zones. These are the resources that take a 'zones' attribute in their definition.
25+
3) Validate that your application code is able to handle the loss of a zone, e.g. that connections are retried properly when a dependency is unreachable.
26+
27+
The _zones_ CLI extension can help with the first two steps. By running this against a specific resource group that contains your production resources, you can be sure that you have not overlooked any resources in your quest for zone redundancy. If the results show 'No' on one of your resources, that means that you need to change the configuration to enable ZR. If it shows 'Never', that probably means you need to deploy multiple of those resources to the different zones manually.
28+
29+
The third step can be validated using Chaos Engineering practices. On Azure, look into Chaos Studio to get started with that.
30+
31+
Suggested use for this extension:
32+
- Manually run this against the production subscription or resource group(s) to validate that all resources have zone redundanct enabled
33+
- Run this as part of your CI/CD pipelines, validating zone redundancy of the resources after deployment in the (pre-)production environment. Consider failing the pipeline if any of the resource results contains _No_ as the result. Note that _no_ only occurs in cases where zone redundancy was not enabled, but could be if the resource was configured differently.
34+
35+
## USAGE
36+
37+
Validate all resources in current scope to which you have read access:
38+
39+
```bash
40+
az zones validate
41+
```
42+
43+
Get the results in human-readable table format:
44+
45+
```bash
46+
az zones validate --output table
47+
```
48+
49+
Validate all resources in specific resource groups to which you have read access:
50+
51+
```bash
52+
az zones validate --resource-groups <resource_group1>,<resource_group2>,...
53+
```
54+
55+
Omit 'dependent' resources from the output. These are resources that by themselves cannot be zone redundant, but take on the status of their parent or related resource. This can be useful for improving readability of the results:
56+
57+
```bash
58+
az zones validate --omit-dependent-resources
59+
```
60+
61+
Validate all resources with specific tags. Resources that have ALL specified tags will be returned
62+
63+
```bash
64+
az zones validate --tags env=prod,criticality=high
65+
```
66+
67+
## Important Notes
68+
69+
- The extension still has missing resource types. These are shown as _Unknown_ in the results. It is essential that you validate zone redundancy of these resources yourself, since your whole application is only zone redundant is all resources are zone redundant.
70+
71+
- The _zones_ CLI extension can only help with resources you can view, i.e. for which you have read access. You must ensure that all relevant resources are indeed listed in the results.
72+
73+
- While this extension is a useful tool in validating zone redundancy on resources, you are still responsible for reviewing the [Reliability Guides](https://learn.microsoft.com/azure/reliability/overview-reliability-guidance) for all the services you use in your applications, as these may contain important information regarding operation in high availability scenarios. Ultimately, the product reliability guides are the authoritative source for zone redundancy guidance.
74+
75+
- Zonal services are considered to be Zone Redundant if they are deployed to at least 2 zones.

src/zones/azext_zones/__init__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
7+
import importlib
8+
from pathlib import Path
9+
from azure.cli.core import AzCommandsLoader
10+
from azext_zones._help import helps # pylint: disable=unused-import
11+
12+
# Import all the resource type validator modules dynamically:
13+
validators_dir = Path(__file__).parent / "resource_type_validators"
14+
for file in validators_dir.glob("*.py"):
15+
if file.name != "__init__.py":
16+
module_name = f".resource_type_validators.{file.stem}"
17+
importlib.import_module(module_name, package=__package__)
18+
19+
20+
class ZonesCommandsLoader(AzCommandsLoader):
21+
22+
def __init__(self, cli_ctx=None):
23+
from azure.cli.core.commands import CliCommandType
24+
from azext_zones._client_factory import cf_zones
25+
zones_custom = CliCommandType(
26+
operations_tmpl='azext_zones.custom#{}',
27+
client_factory=cf_zones)
28+
super(ZonesCommandsLoader, self).__init__(cli_ctx=cli_ctx,
29+
custom_command_type=zones_custom)
30+
31+
def load_command_table(self, args):
32+
from azext_zones.commands import load_command_table
33+
load_command_table(self, args)
34+
return self.command_table
35+
36+
def load_arguments(self, command):
37+
from azext_zones._params import load_arguments
38+
load_arguments(self, command)
39+
40+
41+
COMMAND_LOADER_CLS = ZonesCommandsLoader
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import json
7+
from collections import OrderedDict
8+
from knack.util import todict
9+
from knack.log import get_logger
10+
11+
from .vendored_sdks.resourcegraph.models import ResultTruncated
12+
from .vendored_sdks.resourcegraph.models import QueryRequest, QueryRequestOptions, QueryResponse, ResultFormat, Error
13+
from azure.cli.core._profile import Profile
14+
from azure.core.exceptions import HttpResponseError
15+
from azure.cli.core.azclierror import BadRequestError, AzureInternalError
16+
17+
18+
__SUBSCRIPTION_LIMIT = 1000
19+
__MANAGEMENT_GROUP_LIMIT = 10
20+
__logger = get_logger(__name__)
21+
22+
23+
def build_arg_query(resource_groups, tags):
24+
# type: (list[str], list[str]) -> str
25+
26+
query = "Resources"
27+
if resource_groups is not None and len(resource_groups) > 0:
28+
query += " | where resourceGroup in ({0})".format(','.join(f"'{item}'" for item in resource_groups.split(',')))
29+
30+
if tags is not None:
31+
tagquery = []
32+
for tag in tags.split(','):
33+
tag = tag.strip()
34+
if not tag: # Skip empty tags
35+
continue
36+
37+
if '=' in tag:
38+
# Tag with a value (TagA=ValueA)
39+
tag_name, tag_value = tag.split('=', 1)
40+
# Escape single quotes in the value
41+
tag_value = tag_value.replace("'", "''")
42+
tagquery.append(f"tags['{tag_name}'] == '{tag_value}'")
43+
else:
44+
# Tag without a value. We don't support those.
45+
pass
46+
47+
if tagquery: # Only proceed if tagquery has items
48+
query += " | where " + " and ".join(tagquery)
49+
50+
return query
51+
52+
53+
def execute_arg_query(
54+
client, graph_query, first, skip, subscriptions, management_groups, allow_partial_scopes, skip_token):
55+
56+
mgs_list = management_groups
57+
if mgs_list is not None and len(mgs_list) > __MANAGEMENT_GROUP_LIMIT:
58+
mgs_list = mgs_list[:__MANAGEMENT_GROUP_LIMIT]
59+
warning_message = "The query included more management groups than allowed. "\
60+
"Only the first {0} management groups were included for the results. "\
61+
"To use more than {0} management groups, "\
62+
"see the docs for examples: "\
63+
"https://aka.ms/arg-error-toomanysubs".format(__MANAGEMENT_GROUP_LIMIT)
64+
__logger.warning(warning_message)
65+
66+
subs_list = None
67+
if mgs_list is None:
68+
subs_list = subscriptions or _get_cached_subscriptions()
69+
if subs_list is not None and len(subs_list) > __SUBSCRIPTION_LIMIT:
70+
subs_list = subs_list[:__SUBSCRIPTION_LIMIT]
71+
warning_message = "The query included more subscriptions than allowed. "\
72+
"Only the first {0} subscriptions were included for the results. "\
73+
"To use more than {0} subscriptions, "\
74+
"see the docs for examples: "\
75+
"https://aka.ms/arg-error-toomanysubs".format(__SUBSCRIPTION_LIMIT)
76+
__logger.warning(warning_message)
77+
78+
response = None
79+
try:
80+
result_truncated = False
81+
82+
request_options = QueryRequestOptions(
83+
top=first,
84+
skip=skip,
85+
skip_token=skip_token,
86+
result_format=ResultFormat.object_array,
87+
allow_partial_scopes=allow_partial_scopes
88+
)
89+
90+
request = QueryRequest(
91+
query=graph_query,
92+
subscriptions=subs_list,
93+
management_groups=mgs_list,
94+
options=request_options)
95+
response = client.resources(request) # type: QueryResponse
96+
if response.result_truncated == ResultTruncated.true:
97+
result_truncated = True
98+
99+
if result_truncated and first is not None and len(response.data) < first:
100+
__logger.warning("Unable to paginate the results of the query. "
101+
"Some resources may be missing from the results. "
102+
"To rewrite the query and enable paging, "
103+
"see the docs for an example: https://aka.ms/arg-results-truncated")
104+
105+
except HttpResponseError as ex:
106+
if ex.model.error.code == 'BadRequest':
107+
raise BadRequestError(json.dumps(_to_dict(ex.model.error), indent=4)) from ex
108+
109+
raise AzureInternalError(json.dumps(_to_dict(ex.model.error), indent=4)) from ex
110+
111+
result_dict = dict()
112+
result_dict['data'] = response.data
113+
result_dict['count'] = response.count
114+
result_dict['total_records'] = response.total_records
115+
result_dict['skip_token'] = response.skip_token
116+
117+
return result_dict
118+
119+
120+
def _get_cached_subscriptions():
121+
# type: () -> list[str]
122+
123+
cached_subs = Profile().load_cached_subscriptions()
124+
return [sub['id'] for sub in cached_subs]
125+
126+
127+
def _to_dict(obj):
128+
if isinstance(obj, Error):
129+
return _to_dict(todict(obj))
130+
131+
if isinstance(obj, dict):
132+
result = OrderedDict()
133+
134+
# Complex objects should be displayed last
135+
sorted_keys = sorted(obj.keys(), key=lambda k: (isinstance(obj[k], dict), isinstance(obj[k], list), k))
136+
for key in sorted_keys:
137+
if obj[key] is None or obj[key] == [] or obj[key] == {}:
138+
continue
139+
140+
result[key] = _to_dict(obj[key])
141+
return result
142+
143+
if isinstance(obj, list):
144+
return [_to_dict(v) for v in obj]
145+
146+
return obj
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
def cf_zones(cli_ctx, _):
7+
from azure.cli.core.commands.client_factory import get_mgmt_service_client
8+
from .vendored_sdks.resourcegraph import ResourceGraphClient
9+
return get_mgmt_service_client(cli_ctx, ResourceGraphClient)

src/zones/azext_zones/_clients.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
from azure.cli.core.util import send_raw_request
7+
from azure.cli.core.commands.client_factory import get_subscription_id
8+
9+
10+
# pylint: disable=too-few-public-methods
11+
class MgmtApiClient():
12+
13+
def query(self, cmd, method, resource, api_version, requestBody):
14+
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
15+
sub_id = get_subscription_id(cmd.cli_ctx)
16+
url_fmt = ("{}/subscriptions/{}/{}?api-version={}")
17+
request_url = url_fmt.format(
18+
management_hostname.strip('/'),
19+
sub_id,
20+
resource,
21+
api_version)
22+
23+
r = send_raw_request(cmd.cli_ctx, method, request_url, body=requestBody)
24+
return r.json()

src/zones/azext_zones/_help.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for license information.
5+
# --------------------------------------------------------------------------------------------
6+
7+
from knack.help_files import helps # pylint: disable=unused-import
8+
9+
10+
helps['zones'] = """
11+
type: group
12+
short-summary: Commands to validate Availability Zone Configuration. Use one of the options below.
13+
"""
14+
15+
helps['zones validate'] = """
16+
type: command
17+
short-summary: Validates zone redundancy status of all resources in the current subscription context for which you have read access.
18+
examples:
19+
- name: Validate zone redundancy status of all resources in the specified resource group
20+
text: |-
21+
az zones validate --resource-groups myProductionRG --omit-dependent
22+
- name: Validate zone redundancy status of all resources in the specified resource group, but omit the dependent/child resources
23+
text: |-
24+
az zones validate --resource-groups myProductionRG --omit-dependent
25+
- name: Validate zone redundancy status of all resources that have ALL the specified tags
26+
text: |-
27+
az zones validate --tags env=prod,criticality=high
28+
"""

0 commit comments

Comments
 (0)