Skip to content

Commit 0eae95c

Browse files
authored
[azure][fix] Warn and ignore HttpResponseError (#2034)
* [azure][fix] Warn and ignore HttpResponseError * - process pool - allow expected errors * fix aws imports * name the variable better
1 parent 6010f39 commit 0eae95c

File tree

9 files changed

+104
-81
lines changed

9 files changed

+104
-81
lines changed

fixlib/fixlib/core/actions.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import threading
22
import time
33
from contextlib import suppress, AbstractContextManager
4+
from itertools import islice
45
from logging import Logger
56
from queue import Queue
67

@@ -18,7 +19,7 @@
1819
from fixlib.args import ArgumentParser
1920
from fixlib.jwt import encode_jwt_to_headers
2021
from fixlib.core.ca import TLSData
21-
from typing import Callable, Dict, Optional, List, Any
22+
from typing import Callable, Dict, Optional, List, Any, Set
2223

2324
from fixlib.types import Json
2425
from fixlib.utils import utc_str
@@ -84,6 +85,61 @@ def child_context(self, *context: str) -> "CoreFeedback":
8485
return self.with_context(*(self.context + list(context)))
8586

8687

88+
@define
89+
class ErrorSummary:
90+
error: str
91+
message: str
92+
info: bool
93+
region: Optional[str] = None
94+
service_actions: Dict[str, Set[str]] = field(factory=dict)
95+
96+
97+
class ErrorAccumulator:
98+
def __init__(self) -> None:
99+
self.regional_errors: Dict[Optional[str], Dict[str, ErrorSummary]] = {}
100+
101+
def add_error(
102+
self, as_info: bool, error_kind: str, service: str, action: str, message: str, region: Optional[str] = None
103+
) -> None:
104+
if region not in self.regional_errors:
105+
self.regional_errors[region] = {}
106+
regional_errors = self.regional_errors[region]
107+
108+
key = f"{error_kind}:{message}:{as_info}"
109+
if key not in regional_errors:
110+
regional_errors[key] = ErrorSummary(error_kind, message, as_info, region, {service: {action}})
111+
else:
112+
summary = regional_errors[key]
113+
if service not in summary.service_actions:
114+
summary.service_actions[service] = {action}
115+
else:
116+
summary.service_actions[service].add(action)
117+
118+
def report_region(self, core_feedback: CoreFeedback, region: Optional[str]) -> None:
119+
if regional_errors := self.regional_errors.get(region):
120+
# reset errors for this region
121+
self.regional_errors[region] = {}
122+
# add region as context
123+
feedback = core_feedback.child_context(region) if region else core_feedback
124+
# send to core
125+
for err in regional_errors.values():
126+
srv_acts = []
127+
for service, actions in islice(err.service_actions.items(), 10):
128+
suffix = " and more" if len(actions) > 3 else ""
129+
srv_acts.append(service + ": " + ", ".join(islice(actions, 3)) + suffix)
130+
message = f"[{err.error}] {err.message} Services and actions affected: {', '.join(srv_acts)}"
131+
if len(err.service_actions) > 10:
132+
message += " and more..."
133+
if err.info:
134+
feedback.info(message)
135+
else:
136+
feedback.error(message)
137+
138+
def report_all(self, core_feedback: CoreFeedback) -> None:
139+
for region in self.regional_errors.keys():
140+
self.report_region(core_feedback, region)
141+
142+
87143
class SuppressWithFeedback(AbstractContextManager[None]):
88144
def __init__(self, message: str, feedback: CoreFeedback, logger: Optional[Logger] = None) -> None:
89145
self.message = message

plugins/aws/fix_plugin_aws/aws_client.py

Lines changed: 3 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,18 @@
33
import logging
44
from datetime import datetime
55
from functools import cached_property
6-
from itertools import islice
7-
from typing import Any, Callable, Dict, List, Optional, Set, TypeVar
6+
from typing import Any, Callable, List, Optional, TypeVar
87

9-
from attr import define, field
108
from botocore.config import Config
119
from botocore.exceptions import ClientError, EndpointConnectionError
1210
from botocore.model import ServiceModel
13-
from fix_plugin_aws.configuration import AwsConfig
1411
from retrying import retry
1512

16-
from fixlib.core.actions import CoreFeedback
13+
from fix_plugin_aws.configuration import AwsConfig
14+
from fixlib.core.actions import ErrorAccumulator
1715
from fixlib.json import value_in_path
1816
from fixlib.types import Json, JsonElement
1917
from fixlib.utils import log_runtime, utc_str
20-
2118
from .utils import global_region_by_partition
2219

2320
log = logging.getLogger("fix.plugins.aws")
@@ -49,67 +46,6 @@ def is_retryable_exception(e: Exception) -> bool:
4946
return False
5047

5148

52-
@define
53-
class ErrorSummary:
54-
error: str
55-
message: str
56-
info: bool
57-
region: Optional[str] = None
58-
service_actions: Dict[str, Set[str]] = field(factory=dict)
59-
60-
61-
class ErrorAccumulator:
62-
def __init__(self) -> None:
63-
self.regional_errors: Dict[Optional[str], Dict[str, ErrorSummary]] = {}
64-
65-
def add_error(
66-
self,
67-
as_info: bool,
68-
error_kind: str,
69-
service: str,
70-
action: str,
71-
message: str,
72-
region: Optional[str],
73-
) -> None:
74-
if region not in self.regional_errors:
75-
self.regional_errors[region] = {}
76-
regional_errors = self.regional_errors[region]
77-
78-
key = f"{error_kind}:{message}:{as_info}"
79-
if key not in regional_errors:
80-
regional_errors[key] = ErrorSummary(error_kind, message, as_info, region, {service: {action}})
81-
else:
82-
summary = regional_errors[key]
83-
if service not in summary.service_actions:
84-
summary.service_actions[service] = {action}
85-
else:
86-
summary.service_actions[service].add(action)
87-
88-
def report_region(self, core_feedback: CoreFeedback, region: Optional[str]) -> None:
89-
if regional_errors := self.regional_errors.get(region):
90-
# reset errors for this region
91-
self.regional_errors[region] = {}
92-
# add region as context
93-
feedback = core_feedback.child_context(region) if region else core_feedback
94-
# send to core
95-
for err in regional_errors.values():
96-
srv_acts = []
97-
for aws_service, actions in islice(err.service_actions.items(), 10):
98-
suffix = " and more" if len(actions) > 3 else ""
99-
srv_acts.append(aws_service + ": " + ", ".join(islice(actions, 3)) + suffix)
100-
message = f"[{err.error}] {err.message} Services and actions affected: {', '.join(srv_acts)}"
101-
if len(err.service_actions) > 10:
102-
message += " and more..."
103-
if err.info:
104-
feedback.info(message)
105-
else:
106-
feedback.error(message)
107-
108-
def report_all(self, core_feedback: CoreFeedback) -> None:
109-
for region in self.regional_errors.keys():
110-
self.report_region(core_feedback, region)
111-
112-
11349
class AwsClient:
11450
def __init__(
11551
self,

plugins/aws/fix_plugin_aws/collector.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import List, Type, Optional, ClassVar, Union
55
from datetime import datetime, timezone
66

7-
from fix_plugin_aws.aws_client import AwsClient, ErrorAccumulator
7+
from fix_plugin_aws.aws_client import AwsClient
88
from fix_plugin_aws.configuration import AwsConfig
99
from fix_plugin_aws.resource import (
1010
apigateway,
@@ -48,7 +48,7 @@
4848
from fix_plugin_aws.resource.base import AwsAccount, AwsApiSpec, AwsRegion, AwsResource, GraphBuilder
4949

5050
from fixlib.baseresources import Cloud, EdgeType, BaseOrganizationalRoot, BaseOrganizationalUnit
51-
from fixlib.core.actions import CoreFeedback
51+
from fixlib.core.actions import CoreFeedback, ErrorAccumulator
5252
from fixlib.core.progress import ProgressDone, ProgressTree
5353
from fixlib.graph import Graph, BySearchCriteria, ByNodeId
5454
from fixlib.proc import set_thread_name

plugins/aws/test/aws_client_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from botocore.exceptions import ClientError
44

5-
from fix_plugin_aws.aws_client import AwsClient, ErrorAccumulator, is_retryable_exception
5+
from fix_plugin_aws.aws_client import AwsClient, is_retryable_exception
66
from fix_plugin_aws.configuration import AwsConfig
7+
from fixlib.core.actions import ErrorAccumulator
78
from test.resources import BotoFileBasedSession, BotoErrorSession
89

910

plugins/azure/fix_plugin_azure/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
import multiprocessing
33
from collections import namedtuple
4-
from concurrent.futures import ThreadPoolExecutor, as_completed
4+
from concurrent.futures import as_completed, ProcessPoolExecutor
55
from typing import Optional, Tuple, Any
66

77
from attr import evolve
@@ -61,6 +61,7 @@ def collect(self) -> None:
6161
)
6262
for name, ac in account_configs.items()
6363
for subscription in AzureSubscription.list_subscriptions(ac.credentials())
64+
if ac.allowed(subscription.subscription_id)
6465
}
6566
args = list(args_by_subscription_id.values())
6667

@@ -72,7 +73,7 @@ def collect(self) -> None:
7273
self.core_feedback.progress(progress)
7374

7475
# Collect all subscriptions
75-
with ThreadPoolExecutor(max_workers=config.subscription_pool_size) as executor:
76+
with ProcessPoolExecutor(max_workers=config.subscription_pool_size) as executor:
7677
wait_for = [executor.submit(collect_in_process, sub, self.task_data) for sub in args]
7778
for future in as_completed(wait_for):
7879
subscription, graph = future.result()

plugins/azure/fix_plugin_azure/azure_client.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from abc import ABC, abstractmethod
55
from typing import List, MutableMapping, Optional, Any, Union, Dict
66

7-
from attr import define
7+
from attr import define, field
88
from retrying import retry
99
from azure.core.exceptions import (
1010
ClientAuthenticationError,
@@ -21,6 +21,7 @@
2121
from azure.mgmt.resource.resources.models import GenericResource
2222

2323
from fix_plugin_azure.config import AzureCredentials
24+
from fixlib.core.actions import CoreFeedback, ErrorAccumulator
2425
from fixlib.types import Json
2526

2627
log = logging.getLogger("fix.plugins.azure")
@@ -55,6 +56,7 @@ class AzureApiSpec:
5556
query_parameters: List[str] = []
5657
access_path: Optional[str] = None
5758
expect_array: bool = False
59+
expected_error_codes: List[str] = field(factory=list)
5860

5961

6062
class AzureClient(ABC):
@@ -82,9 +84,13 @@ def delete_resource_tag(self, tag_name: str, resource_id: str) -> bool:
8284
def __create_management_client(
8385
credential: AzureCredentials,
8486
subscription_id: str,
87+
core_feedback: Optional[CoreFeedback] = None,
88+
error_accumulator: Optional[ErrorAccumulator] = None,
8589
resource_group: Optional[str] = None,
8690
) -> AzureClient:
87-
return AzureResourceManagementClient(credential, subscription_id, resource_group)
91+
return AzureResourceManagementClient(
92+
credential, subscription_id, resource_group, core_feedback, error_accumulator
93+
)
8894

8995
create = __create_management_client
9096

@@ -95,10 +101,14 @@ def __init__(
95101
credential: AzureCredentials,
96102
subscription_id: str,
97103
location: Optional[str] = None,
104+
core_feedback: Optional[CoreFeedback] = None,
105+
accumulator: Optional[ErrorAccumulator] = None,
98106
) -> None:
99107
self.credential = credential
100108
self.subscription_id = subscription_id
101109
self.location = location
110+
self.core_feedback = core_feedback
111+
self.accumulator = accumulator or ErrorAccumulator()
102112
self.client = ResourceManagementClient(self.credential, self.subscription_id)
103113

104114
def list(self, spec: AzureApiSpec, **kwargs: Any) -> List[Json]:
@@ -175,10 +185,14 @@ def _list_with_retry(self, spec: AzureApiSpec, **kwargs: Any) -> Optional[List[J
175185
if error := e.error:
176186
if error.code == "NoRegisteredProviderFound":
177187
return None # API not available in this region
188+
elif error.code in spec.expected_error_codes:
189+
return None
178190
elif error.code == "BadRequest" and spec.service == "metric":
179191
raise MetricRequestError from e
180-
log.warning(f"[Azure] Error encountered while requesting resource: {e}")
181-
raise e
192+
code = error.code or "Unknown"
193+
self.accumulator.add_error(False, code, spec.service, spec.path, str(e), self.location)
194+
log.warning(f"[Azure] Client Error: status={e.status_code}, error={e.error}, message={e}")
195+
return None
182196
except Exception as e:
183197
log.warning(f"[Azure] called service={spec.service}: hit unexpected error: {e}", exc_info=e)
184198
return None
@@ -271,4 +285,4 @@ def _make_request(self, url: str, params: MutableMapping[str, Any], headers: Mut
271285
return response
272286

273287
def for_location(self, location: str) -> AzureClient:
274-
return AzureClient.create(self.credential, self.subscription_id, location)
288+
return AzureClient.create(self.credential, self.subscription_id, self.core_feedback, self.accumulator, location)

plugins/azure/fix_plugin_azure/collector.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
)
2727
from fix_plugin_azure.resource.containerservice import resources as aks_resources
2828
from fixlib.baseresources import Cloud, GraphRoot
29-
from fixlib.core.actions import CoreFeedback
29+
from fixlib.core.actions import CoreFeedback, ErrorAccumulator
3030
from fixlib.graph import Graph
3131
from fixlib.json import value_in_path
3232
from fixlib.threading import ExecutorQueue, GatherFutures
@@ -72,7 +72,13 @@ def collect(self) -> None:
7272
) as executor:
7373
self.core_feedback.progress_done(self.subscription.subscription_id, 0, 1, context=[self.cloud.id])
7474
queue = ExecutorQueue(executor, "azure_collector")
75-
client = AzureClient.create(self.credentials, self.subscription.subscription_id)
75+
error_accumulator = ErrorAccumulator()
76+
client = AzureClient.create(
77+
self.credentials,
78+
self.subscription.subscription_id,
79+
core_feedback=self.core_feedback,
80+
error_accumulator=error_accumulator,
81+
)
7682

7783
def get_last_run() -> Optional[datetime]:
7884
td = self.task_data
@@ -123,7 +129,8 @@ def get_last_run() -> Optional[datetime]:
123129

124130
# delete unnecessary nodes after all work is completed
125131
self.after_collect_filter()
126-
132+
# report all accumulated errors
133+
error_accumulator.report_all(self.core_feedback)
127134
self.core_feedback.progress_done(self.subscription.subscription_id, 1, 1, context=[self.cloud.id])
128135
log.info(f"[Azure:{self.subscription.safe_name}] Collecting resources done.")
129136

plugins/azure/fix_plugin_azure/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ def credentials(self) -> AzureCredentials:
4242

4343
return DefaultAzureCredential()
4444

45+
def allowed(self, subscription_id: str) -> bool:
46+
if self.subscriptions is not None:
47+
return subscription_id in self.subscriptions
48+
if self.exclude_subscriptions is not None:
49+
return subscription_id not in self.exclude_subscriptions
50+
return True
51+
4552

4653
@define
4754
class AzureConfig:

plugins/azure/fix_plugin_azure/resource/network.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4701,6 +4701,7 @@ class AzureUsage(AzureResource, BaseNetworkQuota):
47014701
query_parameters=["api-version"],
47024702
access_path="value",
47034703
expect_array=True,
4704+
expected_error_codes=["SubscriptionHasNoUsages"],
47044705
)
47054706
mapping: ClassVar[Dict[str, Bender]] = {
47064707
"id": S("id"),

0 commit comments

Comments
 (0)