pyavd 6.2.0.dev2__py3-none-any.whl → 6.3.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyavd/__init__.py +1 -1
- pyavd/_anta/input_factories/hardware.py +5 -1
- pyavd/_cv/api/arista/alert/v1/__init__.py +40 -3
- pyavd/_cv/api/arista/endpointlocation/v1/__init__.py +10 -0
- pyavd/_cv/api/arista/imagestatus/v1/__init__.py +20 -0
- pyavd/_cv/api/arista/studio/v1/__init__.py +19 -6
- pyavd/_cv/api/arista/workspace/v1/__init__.py +43 -25
- pyavd/_cv/client/__init__.py +77 -17
- pyavd/_cv/client/async_decorators.py +59 -1
- pyavd/_cv/client/exceptions.py +4 -0
- pyavd/_cv/client/models.py +11 -0
- pyavd/_cv/client/studio_topology.py +121 -0
- pyavd/_cv/client/workspace.py +31 -1
- pyavd/_cv/constants.py +23 -0
- pyavd/_cv/schema/__init__.py +48 -0
- pyavd/_cv/schema/cv_deploy.schema.pickle +0 -0
- pyavd/_cv/workflows/deploy_configs_to_cv.py +90 -4
- pyavd/_cv/workflows/deploy_cv_pathfinder_metadata_to_cv.py +1 -1
- pyavd/_cv/workflows/deploy_static_config_studio_manifest_to_cv.py +483 -99
- pyavd/_cv/workflows/deploy_tags_to_cv.py +2 -2
- pyavd/_cv/workflows/deploy_to_cv.py +33 -33
- pyavd/_cv/workflows/finalize_change_control_on_cv.py +0 -4
- pyavd/_cv/workflows/finalize_workspace_on_cv.py +1 -1
- pyavd/_cv/workflows/models.py +233 -73
- pyavd/_cv/workflows/utils.py +52 -0
- pyavd/_cv/workflows/verify_devices_on_cv.py +18 -21
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__address_locking.py +22 -9
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__cvx.py +39 -2
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__dot1x.py +8 -34
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__errdisable.py +45 -6
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__ethernet_interfaces.py +27 -5
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__maintenance.py +8 -4
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__management_interfaces.py +33 -23
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__management_security.py +187 -46
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__monitor_layer1.py +3 -2
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__monitor_loop_protection.py +3 -2
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__mpls.py +26 -7
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__mpls_and_ldp.py +60 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__mpls_rsvp.py +144 -135
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__ntp.py +18 -3
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__port_channel_interfaces.py +8 -4
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__qos.py +12 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__router_bgp.py +48 -19
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__router_ospf.py +65 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__spanning_tree.py +6 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__traffic_policies.py +33 -6
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__tunnel_interfaces.py +26 -16
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__vlan_interfaces.py +44 -6
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__address_locking.py +8 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__cvx.py +68 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__daemon_terminattr.py +6 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__errdisable.py +67 -7
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__ethernet_interfaces.py +25 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__ip_routing_vrfs.py +16 -3
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__maintenance.py +11 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__management_interfaces.py +20 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__management_security.py +194 -2
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__monitor_layer1.py +3 -2
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__mpls.py +60 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__mpls_rsvp.py +204 -185
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__ntp.py +5 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__port_channel_interfaces.py +31 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__qos.py +16 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__router_bgp.py +22 -3
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__router_ospf.py +24 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__spanning_tree.py +4 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__traffic_policies.py +15 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__tunnel_interfaces.py +20 -1
- pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__vlan_interfaces.py +45 -3
- pyavd/_eos_cli_config_gen/schema/__init__.py +3404 -55
- pyavd/_eos_cli_config_gen/schema/eos_cli_config_gen.schema.pickle +0 -0
- pyavd/_eos_designs/eos_designs_facts/schema/eos_designs.schema.pickle +0 -0
- pyavd/_eos_designs/eos_designs_facts/schema/protocol.py +24 -20
- pyavd/_eos_designs/eos_designs_facts/uplinks.py +43 -32
- pyavd/_eos_designs/eos_designs_facts/vlans.py +123 -30
- pyavd/_eos_designs/schema/__init__.py +17122 -2922
- pyavd/_eos_designs/schema/eos_designs.schema.pickle +0 -0
- pyavd/_eos_designs/shared_utils/filtered_tenants.py +32 -19
- pyavd/_eos_designs/shared_utils/inband_management.py +7 -2
- pyavd/_eos_designs/shared_utils/mgmt.py +19 -10
- pyavd/_eos_designs/shared_utils/misc.py +93 -35
- pyavd/_eos_designs/shared_utils/node_type.py +4 -2
- pyavd/_eos_designs/shared_utils/utils.py +9 -2
- pyavd/_eos_designs/structured_config/base/__init__.py +45 -512
- pyavd/_eos_designs/structured_config/base/aaa_settings.py +196 -0
- pyavd/_eos_designs/structured_config/base/daemon_terminattr.py +2 -2
- pyavd/_eos_designs/structured_config/base/dns_settings.py +52 -0
- pyavd/_eos_designs/structured_config/base/dot1x.py +88 -0
- pyavd/_eos_designs/structured_config/base/logging.py +83 -0
- pyavd/_eos_designs/structured_config/base/management_interface.py +57 -0
- pyavd/_eos_designs/structured_config/base/monitor_connectivity.py +71 -0
- pyavd/_eos_designs/structured_config/base/monitor_sessions.py +41 -4
- pyavd/_eos_designs/structured_config/base/ptp.py +126 -0
- pyavd/_eos_designs/structured_config/base/router_bgp.py +66 -0
- pyavd/_eos_designs/structured_config/base/snmp_server.py +0 -12
- pyavd/_eos_designs/structured_config/base/utils.py +3 -3
- pyavd/_eos_designs/structured_config/connected_endpoints/__init__.py +2 -0
- pyavd/_eos_designs/structured_config/connected_endpoints/ethernet_interfaces.py +17 -2
- pyavd/_eos_designs/structured_config/connected_endpoints/mac_access_lists.py +88 -0
- pyavd/_eos_designs/structured_config/connected_endpoints/port_channel_interfaces.py +39 -8
- pyavd/_eos_designs/structured_config/connected_endpoints/utils.py +22 -1
- pyavd/_eos_designs/structured_config/constants.py +0 -16
- pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/utils.py +2 -2
- pyavd/_eos_designs/structured_config/inband_management/__init__.py +104 -157
- pyavd/_eos_designs/structured_config/metadata/digital_twin.py +1 -3
- pyavd/_eos_designs/structured_config/mlag/__init__.py +2 -2
- pyavd/_eos_designs/structured_config/network_services/ip_access_lists.py +8 -0
- pyavd/_eos_designs/structured_config/network_services/router_bgp.py +6 -1
- pyavd/_eos_designs/structured_config/network_services/spanning_tree.py +7 -3
- pyavd/_eos_designs/structured_config/network_services/utils_zscaler.py +8 -2
- pyavd/_eos_designs/structured_config/network_services/vlan_interfaces.py +37 -0
- pyavd/_eos_designs/structured_config/network_services/vlans.py +1 -1
- pyavd/_eos_designs/structured_config/structured_config_utils/__init__.py +56 -0
- pyavd/_eos_designs/structured_config/structured_config_utils/mlag.py +93 -0
- pyavd/_eos_designs/structured_config/structured_config_utils/sflow.py +69 -0
- pyavd/_eos_designs/structured_config/structured_config_utils/underlay.py +124 -0
- pyavd/_eos_designs/structured_config/structured_config_utils/utils.py +44 -0
- pyavd/_eos_designs/structured_config/underlay/dhcp_servers.py +2 -2
- pyavd/_eos_designs/structured_config/underlay/ethernet_interfaces.py +4 -0
- pyavd/_eos_designs/structured_config/underlay/loopback_interfaces.py +30 -6
- pyavd/_eos_designs/structured_config/underlay/port_channel_interfaces.py +3 -0
- pyavd/_eos_designs/structured_config/underlay/router_isis.py +10 -6
- pyavd/_eos_designs/structured_config/underlay/utils.py +4 -3
- pyavd/_schema/avd_meta_schema.pickle +0 -0
- pyavd/_schema/schemas.json.gz +0 -0
- pyavd/_utils/password_utils/password.py +0 -38
- pyavd/api/fabric_documentation/__init__.py +1 -1
- pyavd/api/interface_descriptions/__init__.py +18 -0
- pyavd/get_fabric_documentation.py +2 -1
- {pyavd-6.2.0.dev2.dist-info → pyavd-6.3.0.dev1.dist-info}/METADATA +3 -3
- {pyavd-6.2.0.dev2.dist-info → pyavd-6.3.0.dev1.dist-info}/RECORD +134 -119
- pyavd/_eos_designs/structured_config/structured_config_utils.py +0 -317
- {pyavd-6.2.0.dev2.dist-info → pyavd-6.3.0.dev1.dist-info}/WHEEL +0 -0
- {pyavd-6.2.0.dev2.dist-info → pyavd-6.3.0.dev1.dist-info}/licenses/pyavd/LICENSE +0 -0
- {pyavd-6.2.0.dev2.dist-info → pyavd-6.3.0.dev1.dist-info}/top_level.txt +0 -0
pyavd/__init__.py
CHANGED
|
@@ -18,7 +18,7 @@ PYAVD_PRERELEASE = "" # Set this to aN or bN for alpha and beta releases of pya
|
|
|
18
18
|
__author__ = "Arista Networks"
|
|
19
19
|
__copyright__ = "Copyright 2023-2026 Arista Networks"
|
|
20
20
|
__license__ = "Apache 2.0"
|
|
21
|
-
__version__ = "6.
|
|
21
|
+
__version__ = "6.3.0.dev1"
|
|
22
22
|
|
|
23
23
|
__all__ = [
|
|
24
24
|
"get_avd_facts",
|
|
@@ -80,7 +80,11 @@ class VerifyTransceiversManufacturersInputFactory(AntaTestInputFactory[VerifyTra
|
|
|
80
80
|
@skip_if_hardware_validation_disabled
|
|
81
81
|
def create(self) -> Iterator[VerifyTransceiversManufacturers.Input]:
|
|
82
82
|
"""Generate the inputs for the `VerifyTransceiversManufacturers` test."""
|
|
83
|
-
|
|
83
|
+
validate_hardware = self.structured_config.metadata.validate_hardware
|
|
84
|
+
manufacturers = list(validate_hardware.transceiver_manufacturers)
|
|
85
|
+
if validate_hardware.ignore_no_transceivers and "Not Present" not in manufacturers:
|
|
86
|
+
manufacturers.append("Not Present")
|
|
87
|
+
yield VerifyTransceiversManufacturers.Input(manufacturers=manufacturers)
|
|
84
88
|
|
|
85
89
|
|
|
86
90
|
class VerifyInventoryInputFactory(AntaTestInputFactory[VerifyInventory.Input]):
|
|
@@ -32,6 +32,7 @@ __all__ = (
|
|
|
32
32
|
"Settings",
|
|
33
33
|
"EmailSettings",
|
|
34
34
|
"AzureOAuth",
|
|
35
|
+
"OAuth2ClientCredentials",
|
|
35
36
|
"HttpSettings",
|
|
36
37
|
"HttpHeaders",
|
|
37
38
|
"HeaderValues",
|
|
@@ -959,7 +960,7 @@ class EmailSettings(aristaproto.Message):
|
|
|
959
960
|
@dataclass(eq=False, repr=False)
|
|
960
961
|
class AzureOAuth(aristaproto.Message):
|
|
961
962
|
"""
|
|
962
|
-
AzureOAuth contains the settings for
|
|
963
|
+
AzureOAuth contains the settings for authenticating against Azure using OAuth2
|
|
963
964
|
"""
|
|
964
965
|
|
|
965
966
|
client_id: Optional[str] = aristaproto.message_field(1, wraps=aristaproto.TYPE_STRING)
|
|
@@ -982,6 +983,33 @@ class AzureOAuth(aristaproto.Message):
|
|
|
982
983
|
"""scopes are the scopes that auth is granted for"""
|
|
983
984
|
|
|
984
985
|
|
|
986
|
+
@dataclass(eq=False, repr=False)
|
|
987
|
+
class OAuth2ClientCredentials(aristaproto.Message):
|
|
988
|
+
"""
|
|
989
|
+
OAuth2ClientCredentials contains generic settings for authenticating webhook
|
|
990
|
+
requests using the OAuth2 client credentials (including OpenID Connect) flow.
|
|
991
|
+
"""
|
|
992
|
+
|
|
993
|
+
client_id: Optional[str] = aristaproto.message_field(1, wraps=aristaproto.TYPE_STRING)
|
|
994
|
+
"""client_id of the OAuth2 client"""
|
|
995
|
+
|
|
996
|
+
client_secret: Optional[str] = aristaproto.message_field(2, wraps=aristaproto.TYPE_STRING)
|
|
997
|
+
"""client_secret for the OAuth2 client"""
|
|
998
|
+
|
|
999
|
+
token_url: Optional[str] = aristaproto.message_field(3, wraps=aristaproto.TYPE_STRING)
|
|
1000
|
+
"""
|
|
1001
|
+
token_url is the full URL of the OAuth2/OIDC token endpoint
|
|
1002
|
+
e.g. https://example.com/oauth2/token
|
|
1003
|
+
"""
|
|
1004
|
+
|
|
1005
|
+
scope: Optional[str] = aristaproto.message_field(4, wraps=aristaproto.TYPE_STRING)
|
|
1006
|
+
"""
|
|
1007
|
+
scope is the optional OAuth2 scope string requested for the access token.
|
|
1008
|
+
Multiple scopes can be defined by separating them with spaces,
|
|
1009
|
+
e.g. \"scope1 scope2 scope3\".
|
|
1010
|
+
"""
|
|
1011
|
+
|
|
1012
|
+
|
|
985
1013
|
@dataclass(eq=False, repr=False)
|
|
986
1014
|
class HttpSettings(aristaproto.Message):
|
|
987
1015
|
"""
|
|
@@ -1088,8 +1116,12 @@ class WebhookSettings(aristaproto.Message):
|
|
|
1088
1116
|
|
|
1089
1117
|
azure_o_auth: "AzureOAuth" = aristaproto.message_field(1)
|
|
1090
1118
|
"""
|
|
1091
|
-
azure_o_auth used for auth when using
|
|
1092
|
-
|
|
1119
|
+
azure_o_auth used for auth when using Azure to authenticate webhook requests
|
|
1120
|
+
"""
|
|
1121
|
+
|
|
1122
|
+
oauth2_client_credentials: "OAuth2ClientCredentials" = aristaproto.message_field(2)
|
|
1123
|
+
"""
|
|
1124
|
+
oauth2_client_credentials used for generic OAuth2/OIDC client-credentials auth for webhook
|
|
1093
1125
|
"""
|
|
1094
1126
|
|
|
1095
1127
|
|
|
@@ -1508,6 +1540,11 @@ class Matches(aristaproto.Message):
|
|
|
1508
1540
|
intf_tags is a string tag query that is used to match on the event's interface tags
|
|
1509
1541
|
"""
|
|
1510
1542
|
|
|
1543
|
+
virtual_tags: Optional[str] = aristaproto.message_field(7, wraps=aristaproto.TYPE_STRING)
|
|
1544
|
+
"""
|
|
1545
|
+
virtual_tags is a string tag query that is used to match on the event's virtual tags
|
|
1546
|
+
"""
|
|
1547
|
+
|
|
1511
1548
|
rule_ids: "___fmp__.RepeatedString" = aristaproto.message_field(6)
|
|
1512
1549
|
"""
|
|
1513
1550
|
rule_ids is a list of rule IDs to filter on,
|
|
@@ -206,6 +206,16 @@ class MacType(aristaproto.Enum):
|
|
|
206
206
|
MAC_TYPE_CONFIGURED_STATIC_FRR indicates a MAC configured statically and protected by an EVPN FRR backup tunnel.
|
|
207
207
|
"""
|
|
208
208
|
|
|
209
|
+
VPLS_DYNAMIC_REMOTE = 32
|
|
210
|
+
"""
|
|
211
|
+
MAC_TYPE_VPLS_DYNAMIC_REMOTE indicates a remote MAC learned dynamically from the VPLS.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
CONFIGURED_SYS = 33
|
|
215
|
+
"""
|
|
216
|
+
MAC_TYPE_CONFIGURED_SYS indicates a system MAC address configured on an interface.
|
|
217
|
+
"""
|
|
218
|
+
|
|
209
219
|
OTHER = 99999
|
|
210
220
|
"""MAC_TYPE_OTHER is used for capturing future MAC types."""
|
|
211
221
|
|
|
@@ -13,6 +13,7 @@ __all__ = (
|
|
|
13
13
|
"ErrorCode",
|
|
14
14
|
"WarningCode",
|
|
15
15
|
"InfoCode",
|
|
16
|
+
"ImageSource",
|
|
16
17
|
"SoftwareImage",
|
|
17
18
|
"ImageMetadata",
|
|
18
19
|
"Extension",
|
|
@@ -378,6 +379,25 @@ class InfoCode(aristaproto.Enum):
|
|
|
378
379
|
"""
|
|
379
380
|
|
|
380
381
|
|
|
382
|
+
class ImageSource(aristaproto.Enum):
|
|
383
|
+
"""ImageSource indicates the source type for the image configuration."""
|
|
384
|
+
|
|
385
|
+
UNSPECIFIED = 0
|
|
386
|
+
"""IMAGE_SOURCE_UNSPECIFIED uninitialized value"""
|
|
387
|
+
|
|
388
|
+
STUDIO = 1
|
|
389
|
+
"""IMAGE_SOURCE_STUDIO - image configured from studio"""
|
|
390
|
+
|
|
391
|
+
NETWORK_PROVISIONING = 2
|
|
392
|
+
"""
|
|
393
|
+
IMAGE_SOURCE_NETWORK_PROVISIONING - image configured from
|
|
394
|
+
network provisioning workflow
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
HIERARCHY = 3
|
|
398
|
+
"""IMAGE_SOURCE_HIERARCHY - image configured from hierarchy workflow"""
|
|
399
|
+
|
|
400
|
+
|
|
381
401
|
@dataclass(eq=False, repr=False)
|
|
382
402
|
class SoftwareImage(aristaproto.Message):
|
|
383
403
|
"""
|
|
@@ -243,6 +243,9 @@ class EntityType(aristaproto.Enum):
|
|
|
243
243
|
entity type for static config studio.
|
|
244
244
|
"""
|
|
245
245
|
|
|
246
|
+
DEPENDENCIES = 8
|
|
247
|
+
"""ENTITY_TYPE_DEPENDENCIES indicates the Dependencies entity type."""
|
|
248
|
+
|
|
246
249
|
|
|
247
250
|
class TemplateType(aristaproto.Enum):
|
|
248
251
|
"""
|
|
@@ -578,10 +581,17 @@ class Entities(aristaproto.Message):
|
|
|
578
581
|
|
|
579
582
|
values: Dict[str, "Entity"] = aristaproto.map_field(1, aristaproto.TYPE_STRING, aristaproto.TYPE_MESSAGE)
|
|
580
583
|
"""
|
|
581
|
-
values is a map from
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
584
|
+
values is a map from EntityType enum name to Entity.
|
|
585
|
+
Keys are the EntityType enum names defined below, e.g.:
|
|
586
|
+
|
|
587
|
+
```
|
|
588
|
+
\"ENTITY_TYPE_INPUTS\" -> Entity{
|
|
589
|
+
entity_type: ENTITY_TYPE_INPUTS,
|
|
590
|
+
last_modified_at: 2026-05-07T12:34:56Z,
|
|
591
|
+
last_modified_by: \"admin\",
|
|
592
|
+
removed: false,
|
|
593
|
+
}
|
|
594
|
+
```
|
|
585
595
|
"""
|
|
586
596
|
|
|
587
597
|
|
|
@@ -930,10 +940,13 @@ class FloatInputFieldProps(aristaproto.Message):
|
|
|
930
940
|
`{ fieldId: field_id }`.
|
|
931
941
|
|
|
932
942
|
E.g,
|
|
943
|
+
|
|
944
|
+
```
|
|
933
945
|
[
|
|
934
|
-
|
|
935
|
-
|
|
946
|
+
{ fieldId: floatField1ID },
|
|
947
|
+
{ fieldId: floatField2ID }
|
|
936
948
|
]
|
|
949
|
+
```
|
|
937
950
|
Here, the possible values for the floats identified by
|
|
938
951
|
`floatField1ID` and `floatField2ID` are used as the
|
|
939
952
|
possible values for this float.
|
|
@@ -675,6 +675,9 @@ class EntityType(aristaproto.Enum):
|
|
|
675
675
|
NODE = 13
|
|
676
676
|
"""ENTITY_TYPE_NODE indicates the Node entity type."""
|
|
677
677
|
|
|
678
|
+
DEPENDENCIES = 14
|
|
679
|
+
"""ENTITY_TYPE_DEPENDENCIES indicates the dependencies entity type."""
|
|
680
|
+
|
|
678
681
|
|
|
679
682
|
class DiffType(aristaproto.Enum):
|
|
680
683
|
"""DiffType enumerates types of diff."""
|
|
@@ -855,6 +858,15 @@ class Workspace(aristaproto.Message):
|
|
|
855
858
|
configured to exclude Network Provisioning.
|
|
856
859
|
"""
|
|
857
860
|
|
|
861
|
+
decommission_request_ids: "___fmp__.MapStringString" = aristaproto.message_field(16)
|
|
862
|
+
"""
|
|
863
|
+
decommission_request_ids provides, for each device staged for
|
|
864
|
+
decommission in this workspace, the corresponding request UUID passed
|
|
865
|
+
to inventory.v1.DeviceDecommissioningConfig. These request UUIDs can
|
|
866
|
+
be used to track the status using the inventory.v1.DeviceDecommissioning
|
|
867
|
+
resource.
|
|
868
|
+
"""
|
|
869
|
+
|
|
858
870
|
|
|
859
871
|
@dataclass(eq=False, repr=False)
|
|
860
872
|
class InputError(aristaproto.Message):
|
|
@@ -1084,6 +1096,9 @@ class ImageValidationResult(aristaproto.Message):
|
|
|
1084
1096
|
infos: "__imagestatus_v1__.ImageInfos" = aristaproto.message_field(5)
|
|
1085
1097
|
"""infos are any info messages about the generated image."""
|
|
1086
1098
|
|
|
1099
|
+
image_source: "__imagestatus_v1__.ImageSource" = aristaproto.enum_field(6)
|
|
1100
|
+
"""image_source identifies the source of the image."""
|
|
1101
|
+
|
|
1087
1102
|
|
|
1088
1103
|
@dataclass(eq=False, repr=False)
|
|
1089
1104
|
class ConfigSyncResult(aristaproto.Message):
|
|
@@ -1391,9 +1406,12 @@ class DiffEntry(aristaproto.Message):
|
|
|
1391
1406
|
- value: the element’s identifier
|
|
1392
1407
|
|
|
1393
1408
|
Example:
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1409
|
+
|
|
1410
|
+
```
|
|
1411
|
+
users = [{\"id\":\"u1\",\"name\":\"Alice\"}]
|
|
1412
|
+
key_path = [\"users\", \"[id=u1]\", \"name\"]
|
|
1413
|
+
path = [\"users\", \"0\", \"name\"]
|
|
1414
|
+
```
|
|
1397
1415
|
"""
|
|
1398
1416
|
|
|
1399
1417
|
|
|
@@ -1418,28 +1436,28 @@ class DiffKey(aristaproto.Message):
|
|
|
1418
1436
|
by [key1, value1, key2, value2, ...]
|
|
1419
1437
|
studio_id are well known e.g studio-date-time
|
|
1420
1438
|
e.g entity_ids for entity types
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1439
|
+
studio, inputs, assigned tags, dependencies: [“studio_id”, <id>]
|
|
1440
|
+
buildhook: [“studio_id”, <id>, “hook_id”, <id>]
|
|
1441
|
+
autofill: [“studio_id”, <id>, “input_field_id”, <id>]
|
|
1442
|
+
configlet: [“configlet_id”, <id>]
|
|
1443
|
+
configletassignment: [“configlet_assignment_id”, <id>]
|
|
1444
|
+
tags:
|
|
1445
|
+
element_type is one of “1” (device), “2” (interface)
|
|
1446
|
+
[“creator_type”, “2”, “element_type”, <element_type>,
|
|
1447
|
+
“element_sub_type”, “1”, “label”, <label>, “value”, <value>]
|
|
1448
|
+
tag assignments:
|
|
1449
|
+
For element_type = “1” (device)
|
|
1450
|
+
[“creator_type”, “2”, “element_type”, “1”, “element_sub_type”, “1”,
|
|
1451
|
+
“label”, <label>, “value”, <value>, “device_id”, <id>]
|
|
1452
|
+
For element_type = “2” (interface)
|
|
1453
|
+
[“creator_type”, “2”, “element_type”, “2”, “element_sub_type”, “1”,
|
|
1454
|
+
“label”, <label>, “value”, <value>, “device_id”, <id>,
|
|
1455
|
+
“interface_id”, <id>]
|
|
1456
|
+
hierarchy related entities:
|
|
1457
|
+
node: [“node_id”, <id>]
|
|
1458
|
+
fixture_class: [“fixture_class_id”, <id>]
|
|
1459
|
+
fixture_instance: [“fixture_instance_id”, <id>]
|
|
1460
|
+
processor: [“processor_id”, <id>]
|
|
1443
1461
|
"""
|
|
1444
1462
|
|
|
1445
1463
|
|
pyavd/_cv/client/__init__.py
CHANGED
|
@@ -8,9 +8,12 @@ import platform
|
|
|
8
8
|
import ssl
|
|
9
9
|
import sys
|
|
10
10
|
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
from logging import getLogger
|
|
12
|
+
from os import environ
|
|
11
13
|
from typing import TYPE_CHECKING, Protocol
|
|
12
14
|
|
|
13
15
|
from grpclib.client import Channel
|
|
16
|
+
from grpclib.config import Configuration
|
|
14
17
|
from requests import JSONDecodeError, get, post
|
|
15
18
|
from requests.exceptions import HTTPError, RequestException
|
|
16
19
|
|
|
@@ -18,8 +21,10 @@ from .change_control import ChangeControlMixin
|
|
|
18
21
|
from .configlet import ConfigletMixin
|
|
19
22
|
from .exceptions import CVClientException
|
|
20
23
|
from .inventory import InventoryMixin
|
|
24
|
+
from .models import CVTLSSettings
|
|
21
25
|
from .proxy import HTTPProxyManager
|
|
22
26
|
from .studio import StudioMixin
|
|
27
|
+
from .studio_topology import StudioTopologyMixin
|
|
23
28
|
from .swg import SwgMixin
|
|
24
29
|
from .tag import TagMixin
|
|
25
30
|
from .utils import UtilsMixin
|
|
@@ -32,12 +37,17 @@ if TYPE_CHECKING:
|
|
|
32
37
|
from grpclib.protocol import H2Protocol
|
|
33
38
|
from typing_extensions import Self
|
|
34
39
|
|
|
40
|
+
from pyavd._cv.workflows.models import CVGRPCChannelConfiguration
|
|
41
|
+
|
|
42
|
+
LOGGER = getLogger(__name__)
|
|
43
|
+
|
|
35
44
|
|
|
36
45
|
class CVClientProtocol(
|
|
37
46
|
ChangeControlMixin,
|
|
38
47
|
ConfigletMixin,
|
|
39
48
|
InventoryMixin,
|
|
40
49
|
StudioMixin,
|
|
50
|
+
StudioTopologyMixin,
|
|
41
51
|
SwgMixin,
|
|
42
52
|
TagMixin,
|
|
43
53
|
WorkspaceMixin,
|
|
@@ -51,11 +61,14 @@ class CVClientProtocol(
|
|
|
51
61
|
_servers: list[str]
|
|
52
62
|
_port: int
|
|
53
63
|
_verify_certs: bool
|
|
64
|
+
_use_system_certs: bool
|
|
54
65
|
_token: str | None
|
|
55
66
|
_username: str | None
|
|
56
67
|
_password: str | None
|
|
57
68
|
_cv_version: CvVersion | None = None
|
|
58
69
|
_proxy_manager: HTTPProxyManager | None = None
|
|
70
|
+
_grpc_channel_configuration: CVGRPCChannelConfiguration | None = None
|
|
71
|
+
_tls: CVTLSSettings
|
|
59
72
|
|
|
60
73
|
async def __aenter__(self) -> Self:
|
|
61
74
|
"""Using asynchronous context manager since grpclib must be initialized inside an asyncio loop."""
|
|
@@ -71,9 +84,6 @@ class CVClientProtocol(
|
|
|
71
84
|
# TODO: Verify connection
|
|
72
85
|
# TODO: Handle multinode clusters
|
|
73
86
|
|
|
74
|
-
# Ensure that the default ssl context is initialized before doing any requests.
|
|
75
|
-
ssl_context = self._ssl_context()
|
|
76
|
-
|
|
77
87
|
if not self._token:
|
|
78
88
|
self._set_token()
|
|
79
89
|
|
|
@@ -81,13 +91,13 @@ class CVClientProtocol(
|
|
|
81
91
|
|
|
82
92
|
if self._channel is None:
|
|
83
93
|
if self._proxy_manager is not None:
|
|
84
|
-
self._channel = await self._create_proxy_channel(
|
|
94
|
+
self._channel = await self._create_proxy_channel(self._tls.grpc_ssl)
|
|
85
95
|
else:
|
|
86
|
-
self._channel = Channel(host=self._servers[0], port=self._port, ssl=
|
|
96
|
+
self._channel = Channel(host=self._servers[0], port=self._port, ssl=self._tls.grpc_ssl, config=self._grpclib_channel_config)
|
|
87
97
|
|
|
88
98
|
self._metadata = {"authorization": "Bearer " + self._token}
|
|
89
99
|
|
|
90
|
-
async def _create_proxy_channel(self, ssl_context: ssl.SSLContext | bool) -> Channel:
|
|
100
|
+
async def _create_proxy_channel(self, ssl_context: ssl.SSLContext | ssl.DefaultVerifyPaths | bool) -> Channel:
|
|
91
101
|
"""
|
|
92
102
|
Create a gRPC channel using the proxy manager.
|
|
93
103
|
|
|
@@ -98,7 +108,7 @@ class CVClientProtocol(
|
|
|
98
108
|
Configured gRPC Channel instance.
|
|
99
109
|
"""
|
|
100
110
|
# Create the channel first
|
|
101
|
-
channel = Channel(host=self._servers[0], port=self._port, ssl=ssl_context)
|
|
111
|
+
channel = Channel(host=self._servers[0], port=self._port, ssl=ssl_context, config=self._grpclib_channel_config)
|
|
102
112
|
|
|
103
113
|
# Create custom connector that uses proxy
|
|
104
114
|
async def proxy_connection() -> H2Protocol:
|
|
@@ -126,13 +136,36 @@ class CVClientProtocol(
|
|
|
126
136
|
channel._create_connection = proxy_connection
|
|
127
137
|
return channel
|
|
128
138
|
|
|
129
|
-
|
|
139
|
+
@property
|
|
140
|
+
def _grpclib_channel_config(self) -> Configuration:
|
|
141
|
+
"""Build the grpclib Channel `config` from the optional gRPC channel configuration."""
|
|
142
|
+
if self._grpc_channel_configuration is None:
|
|
143
|
+
return Configuration()
|
|
144
|
+
return self._grpc_channel_configuration.as_grpclib_configuration()
|
|
145
|
+
|
|
146
|
+
def _resolve_tls_settings(self) -> CVTLSSettings:
|
|
130
147
|
"""
|
|
131
|
-
|
|
148
|
+
Resolve TLS settings for grpclib and requests based on `verify_certs` and `use_system_certs`.
|
|
149
|
+
|
|
150
|
+
`verify_certs=False`: No verification on either transport.
|
|
151
|
+
grpclib gets a permissive SSLContext (CERT_NONE, no hostname check).
|
|
152
|
+
requests gets `verify=False`.
|
|
153
|
+
|
|
154
|
+
`verify_certs=True`, `use_system_certs=False`: certifi.
|
|
155
|
+
grpclib gets `True` and resolves to certifi internally.
|
|
156
|
+
requests gets `verify=True`. If `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE` is set, requests uses that bundle instead of certifi (this override only
|
|
157
|
+
applies when `verify is True`, never when it is an explicit path).
|
|
132
158
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
`verify_certs=True`, `use_system_certs=True`: OS trust store via `ssl.get_default_verify_paths()`, which already reads
|
|
160
|
+
`SSL_CERT_FILE` / `SSL_CERT_DIR` and falls back to compiled-in defaults.
|
|
161
|
+
|
|
162
|
+
grpclib gets the full `DefaultVerifyPaths` and loads both `cafile` and `capath`. The result is additive (OS defaults plus any user env overrides).
|
|
163
|
+
|
|
164
|
+
requests takes a single path. Default rule: `cafile or capath`. Override: when user sets `SSL_CERT_DIR` -> return `capath` instead,
|
|
165
|
+
otherwise the OS-default cafile overrides it. This is the reason why resolver reads env vars even though `get_default_verify_paths()`
|
|
166
|
+
already does it behind the scene.
|
|
167
|
+
|
|
168
|
+
On systems with no usable trust store (distroless) -> warn and fall back to certifi for both transports.
|
|
136
169
|
"""
|
|
137
170
|
if not self._verify_certs:
|
|
138
171
|
# Accepting SonarLint issue: We are purposely implementing no verification of certs.
|
|
@@ -140,9 +173,25 @@ class CVClientProtocol(
|
|
|
140
173
|
context.check_hostname = False
|
|
141
174
|
context.verify_mode = ssl.CERT_NONE # NOSONAR
|
|
142
175
|
context.set_alpn_protocols(["h2"])
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
return CVTLSSettings(grpc_ssl=context, requests_verify=False)
|
|
177
|
+
|
|
178
|
+
if self._use_system_certs:
|
|
179
|
+
verify_paths = ssl.get_default_verify_paths()
|
|
180
|
+
user_set_capath_only = "SSL_CERT_DIR" in environ and "SSL_CERT_FILE" not in environ
|
|
181
|
+
if user_set_capath_only and verify_paths.capath:
|
|
182
|
+
return CVTLSSettings(grpc_ssl=verify_paths, requests_verify=verify_paths.capath)
|
|
183
|
+
if path := (verify_paths.cafile or verify_paths.capath):
|
|
184
|
+
return CVTLSSettings(grpc_ssl=verify_paths, requests_verify=path)
|
|
185
|
+
# No usable OS trust store — warn and fall through to the certifi default.
|
|
186
|
+
LOGGER.warning(
|
|
187
|
+
"CVClient: 'use_system_certs' is enabled but no system trust store was found "
|
|
188
|
+
"(neither SSL_CERT_FILE/SSL_CERT_DIR nor OpenSSL's compiled-in default paths "
|
|
189
|
+
"resolve to a readable file or directory). Falling back to the 'certifi' bundle for "
|
|
190
|
+
"both gRPC and REST. To use the OS trust store, install a CA bundle package "
|
|
191
|
+
"(e.g. 'ca-certificates') or set SSL_CERT_FILE / SSL_CERT_DIR explicitly."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return CVTLSSettings(grpc_ssl=True, requests_verify=True)
|
|
146
195
|
|
|
147
196
|
def _set_token(self) -> None:
|
|
148
197
|
"""
|
|
@@ -163,7 +212,7 @@ class CVClientProtocol(
|
|
|
163
212
|
response = post( # noqa: S113 TODO: Add configurable timeout
|
|
164
213
|
"https://" + self._servers[0] + "/cvpservice/login/authenticate.do",
|
|
165
214
|
auth=(self._username, self._password),
|
|
166
|
-
verify=self.
|
|
215
|
+
verify=self._tls.requests_verify,
|
|
167
216
|
proxies=self._proxy_manager.get_requests_proxies() if self._proxy_manager is not None else None,
|
|
168
217
|
json={},
|
|
169
218
|
)
|
|
@@ -194,7 +243,7 @@ class CVClientProtocol(
|
|
|
194
243
|
response = get( # noqa: S113 TODO: Add configurable timeout
|
|
195
244
|
"https://" + self._servers[0] + "/cvpservice/cvpInfo/getCvpInfo.do",
|
|
196
245
|
headers={"Authorization": f"Bearer {self._token}", "User-Agent": self._get_user_agent()},
|
|
197
|
-
verify=self.
|
|
246
|
+
verify=self._tls.requests_verify,
|
|
198
247
|
proxies=self._proxy_manager.get_requests_proxies() if self._proxy_manager is not None else None,
|
|
199
248
|
json={},
|
|
200
249
|
)
|
|
@@ -250,10 +299,12 @@ class CVClient(CVClientProtocol):
|
|
|
250
299
|
password: str | None = None,
|
|
251
300
|
port: int = 443,
|
|
252
301
|
verify_certs: bool = True,
|
|
302
|
+
use_system_certs: bool = False,
|
|
253
303
|
proxy_host: str | None = None,
|
|
254
304
|
proxy_port: int = 8080,
|
|
255
305
|
proxy_username: str | None = None,
|
|
256
306
|
proxy_password: str | None = None,
|
|
307
|
+
grpc_channel_configuration: CVGRPCChannelConfiguration | None = None,
|
|
257
308
|
) -> None:
|
|
258
309
|
"""
|
|
259
310
|
CVClient is a high-level API library for using CloudVision Resource APIs.
|
|
@@ -268,10 +319,15 @@ class CVClient(CVClientProtocol):
|
|
|
268
319
|
password: Password to use for authentication if token is not set.
|
|
269
320
|
port: TCP port to use for the connection.
|
|
270
321
|
verify_certs: Disables SSL certificate verification if set to False. Not recommended for production.
|
|
322
|
+
use_system_certs: Use system certificates and honor overrides with `SSL_CERT_FILE` and
|
|
323
|
+
`SSL_CERT_DIR`. Prefer the OS trust store over the bundled `certifi` Python package
|
|
324
|
+
(certifi is only used as a fallback when the OS provides no usable trust store).
|
|
325
|
+
Applied to both the gRPC channel and the REST calls. Ignored when `verify_certs=False`.
|
|
271
326
|
proxy_host: HTTP proxy hostname.
|
|
272
327
|
proxy_port: HTTP proxy port.
|
|
273
328
|
proxy_username: Proxy authentication username.
|
|
274
329
|
proxy_password: Proxy authentication password.
|
|
330
|
+
grpc_channel_configuration: Optional gRPC channel configuration settings.
|
|
275
331
|
"""
|
|
276
332
|
if isinstance(servers, list):
|
|
277
333
|
self._servers = servers
|
|
@@ -283,7 +339,11 @@ class CVClient(CVClientProtocol):
|
|
|
283
339
|
self._username = username
|
|
284
340
|
self._password = password
|
|
285
341
|
self._verify_certs = verify_certs
|
|
342
|
+
self._use_system_certs = use_system_certs
|
|
343
|
+
self._grpc_channel_configuration = grpc_channel_configuration
|
|
286
344
|
self._proxy_manager = None
|
|
345
|
+
# Resolve TLS settings.
|
|
346
|
+
self._tls = self._resolve_tls_settings()
|
|
287
347
|
|
|
288
348
|
# Initialize proxy manager if proxy is configured
|
|
289
349
|
if proxy_host is not None:
|
|
@@ -16,7 +16,8 @@ from typing import TYPE_CHECKING, Any, ClassVar, ParamSpec, TypeVar, get_args, g
|
|
|
16
16
|
from grpclib import Status
|
|
17
17
|
from grpclib.exceptions import GRPCError, StreamTerminatedError
|
|
18
18
|
|
|
19
|
-
from pyavd._cv.client.exceptions import CVClientBulkAPIError, CVClientException, CVGRPCError, CVResourceNotFound, CVTimeoutError
|
|
19
|
+
from pyavd._cv.client.exceptions import CVClientBulkAPIError, CVClientException, CVClientInvalidServerName, CVGRPCError, CVResourceNotFound, CVTimeoutError
|
|
20
|
+
from pyavd._cv.constants import CV_REGION_TO_SERVER_MAP, CVAAS_API_PREFIX, CVAAS_STREAMING_PREFIX
|
|
20
21
|
from pyavd._utils import batch
|
|
21
22
|
|
|
22
23
|
from .constants import CVAAS_VERSION_STRING
|
|
@@ -269,6 +270,17 @@ class GRPCRequestHandler:
|
|
|
269
270
|
new_exception.size = int(matches.group("size"))
|
|
270
271
|
raise new_exception
|
|
271
272
|
|
|
273
|
+
case Status.UNKNOWN:
|
|
274
|
+
caller = call_args[0]
|
|
275
|
+
invalid_cvaas_fqdn, hint_msg = self._invalid_cvaas_fqdn(
|
|
276
|
+
getattr(caller, "_servers", []),
|
|
277
|
+
getattr(caller, "_cv_version", None) or CvVersion(CVAAS_VERSION_STRING),
|
|
278
|
+
)
|
|
279
|
+
if invalid_cvaas_fqdn:
|
|
280
|
+
raise CVClientInvalidServerName(hint_msg)
|
|
281
|
+
|
|
282
|
+
# gRPC UNKNOWN received from non-CVaaS endpoint or correctly configured CVaaS
|
|
283
|
+
raise CVGRPCError(*e.args, call_args, call_kwargs)
|
|
272
284
|
case _:
|
|
273
285
|
# All other gRPC errors are converted to CVGRPCError
|
|
274
286
|
raise CVGRPCError(*e.args, call_args, call_kwargs)
|
|
@@ -383,3 +395,49 @@ class GRPCRequestHandler:
|
|
|
383
395
|
|
|
384
396
|
if found_errors:
|
|
385
397
|
raise CVClientBulkAPIError(func_name, found_errors)
|
|
398
|
+
|
|
399
|
+
def _invalid_cvaas_fqdn(self, cv_servers: list[str], cv_version: CvVersion) -> tuple[bool, str]:
|
|
400
|
+
"""
|
|
401
|
+
Check if targeted CVaaS FQDN is invalid.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
cv_servers: List of configured CloudVision server FQDNs. Only the first entry is inspected.
|
|
405
|
+
cv_version: Version negotiated with the connected CloudVision server. Used to suppress CVaaS-specific
|
|
406
|
+
hints when an arista.io FQDN actually resolves to an on-prem CVP (spoofed DNS zone).
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
A tuple of <bool> and <str>, indicating if FQDN of the API endpoint is invalid and a hint explaining invalidity details and possible mitigation.
|
|
410
|
+
"""
|
|
411
|
+
first_cv_server = cv_servers[0] if cv_servers else ""
|
|
412
|
+
if not first_cv_server.endswith("arista.io"):
|
|
413
|
+
return False, ""
|
|
414
|
+
|
|
415
|
+
# Guard against locally-spoofed arista.io DNS zone pointing to an on-prem CVP
|
|
416
|
+
if cv_version.version != CVAAS_VERSION_STRING:
|
|
417
|
+
return False, ""
|
|
418
|
+
|
|
419
|
+
base_fqdns = set(CV_REGION_TO_SERVER_MAP.values())
|
|
420
|
+
prefix, _, base = first_cv_server.partition(".")
|
|
421
|
+
|
|
422
|
+
if base in base_fqdns:
|
|
423
|
+
# Correctly configured API endpoint
|
|
424
|
+
if prefix == CVAAS_API_PREFIX:
|
|
425
|
+
return False, ""
|
|
426
|
+
# Target CVaaS is pointing to the streaming endpoint
|
|
427
|
+
if prefix == CVAAS_STREAMING_PREFIX:
|
|
428
|
+
return True, (
|
|
429
|
+
f"CVaaS FQDN '{first_cv_server}' is pointing to the streaming endpoint. Please use API endpoint '{CVAAS_API_PREFIX}.{base}' instead."
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Target CVaaS is missing api prefix
|
|
433
|
+
if first_cv_server in base_fqdns:
|
|
434
|
+
return True, (
|
|
435
|
+
f"CVaaS FQDN '{first_cv_server}' is missing the required '{CVAAS_API_PREFIX}.' prefix. "
|
|
436
|
+
f"Please use '{CVAAS_API_PREFIX}.{first_cv_server}' instead."
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Unknown arista.io FQDN
|
|
440
|
+
return True, (
|
|
441
|
+
f"Provided CVaaS FQDN '{first_cv_server}' may be incorrect. "
|
|
442
|
+
"Please check 'https://www.arista.io/help' for the full list of supported CVaaS clusters."
|
|
443
|
+
)
|
pyavd/_cv/client/exceptions.py
CHANGED
pyavd/_cv/client/models.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright (c) 2025-2026 Arista Networks, Inc.
|
|
2
2
|
# Use of this source code is governed by the Apache License 2.0
|
|
3
3
|
# that can be found in the LICENSE file.
|
|
4
|
+
import ssl
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from typing import Literal
|
|
6
7
|
|
|
@@ -21,6 +22,16 @@ ELEMENT_TYPE_TO_STRING_MAP = {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class CVTLSSettings:
|
|
27
|
+
"""Resolved TLS settings for a CVClient, used for the gRPC channel and REST calls."""
|
|
28
|
+
|
|
29
|
+
grpc_ssl: ssl.SSLContext | ssl.DefaultVerifyPaths | bool
|
|
30
|
+
"""Value passed to grpclib's `Channel(ssl=...)`."""
|
|
31
|
+
requests_verify: bool | str
|
|
32
|
+
"""Value passed to `requests` as `verify=...`."""
|
|
33
|
+
|
|
34
|
+
|
|
24
35
|
@dataclass(frozen=True)
|
|
25
36
|
class CVTag:
|
|
26
37
|
"""Represent the input model for a CloudVision Tag."""
|