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.
Files changed (135) hide show
  1. pyavd/__init__.py +1 -1
  2. pyavd/_anta/input_factories/hardware.py +5 -1
  3. pyavd/_cv/api/arista/alert/v1/__init__.py +40 -3
  4. pyavd/_cv/api/arista/endpointlocation/v1/__init__.py +10 -0
  5. pyavd/_cv/api/arista/imagestatus/v1/__init__.py +20 -0
  6. pyavd/_cv/api/arista/studio/v1/__init__.py +19 -6
  7. pyavd/_cv/api/arista/workspace/v1/__init__.py +43 -25
  8. pyavd/_cv/client/__init__.py +77 -17
  9. pyavd/_cv/client/async_decorators.py +59 -1
  10. pyavd/_cv/client/exceptions.py +4 -0
  11. pyavd/_cv/client/models.py +11 -0
  12. pyavd/_cv/client/studio_topology.py +121 -0
  13. pyavd/_cv/client/workspace.py +31 -1
  14. pyavd/_cv/constants.py +23 -0
  15. pyavd/_cv/schema/__init__.py +48 -0
  16. pyavd/_cv/schema/cv_deploy.schema.pickle +0 -0
  17. pyavd/_cv/workflows/deploy_configs_to_cv.py +90 -4
  18. pyavd/_cv/workflows/deploy_cv_pathfinder_metadata_to_cv.py +1 -1
  19. pyavd/_cv/workflows/deploy_static_config_studio_manifest_to_cv.py +483 -99
  20. pyavd/_cv/workflows/deploy_tags_to_cv.py +2 -2
  21. pyavd/_cv/workflows/deploy_to_cv.py +33 -33
  22. pyavd/_cv/workflows/finalize_change_control_on_cv.py +0 -4
  23. pyavd/_cv/workflows/finalize_workspace_on_cv.py +1 -1
  24. pyavd/_cv/workflows/models.py +233 -73
  25. pyavd/_cv/workflows/utils.py +52 -0
  26. pyavd/_cv/workflows/verify_devices_on_cv.py +18 -21
  27. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__address_locking.py +22 -9
  28. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__cvx.py +39 -2
  29. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__dot1x.py +8 -34
  30. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__errdisable.py +45 -6
  31. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__ethernet_interfaces.py +27 -5
  32. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__maintenance.py +8 -4
  33. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__management_interfaces.py +33 -23
  34. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__management_security.py +187 -46
  35. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__monitor_layer1.py +3 -2
  36. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__monitor_loop_protection.py +3 -2
  37. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__mpls.py +26 -7
  38. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__mpls_and_ldp.py +60 -1
  39. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__mpls_rsvp.py +144 -135
  40. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__ntp.py +18 -3
  41. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__port_channel_interfaces.py +8 -4
  42. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__qos.py +12 -1
  43. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__router_bgp.py +48 -19
  44. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__router_ospf.py +65 -1
  45. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__spanning_tree.py +6 -1
  46. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__traffic_policies.py +33 -6
  47. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__tunnel_interfaces.py +26 -16
  48. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/documentation__vlan_interfaces.py +44 -6
  49. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__address_locking.py +8 -1
  50. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__cvx.py +68 -1
  51. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__daemon_terminattr.py +6 -1
  52. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__errdisable.py +67 -7
  53. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__ethernet_interfaces.py +25 -1
  54. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__ip_routing_vrfs.py +16 -3
  55. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__maintenance.py +11 -1
  56. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__management_interfaces.py +20 -1
  57. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__management_security.py +194 -2
  58. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__monitor_layer1.py +3 -2
  59. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__mpls.py +60 -1
  60. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__mpls_rsvp.py +204 -185
  61. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__ntp.py +5 -1
  62. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__port_channel_interfaces.py +31 -1
  63. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__qos.py +16 -1
  64. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__router_bgp.py +22 -3
  65. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__router_ospf.py +24 -1
  66. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__spanning_tree.py +4 -1
  67. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__traffic_policies.py +15 -1
  68. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__tunnel_interfaces.py +20 -1
  69. pyavd/_eos_cli_config_gen/j2templates/compiled_templates/eos__vlan_interfaces.py +45 -3
  70. pyavd/_eos_cli_config_gen/schema/__init__.py +3404 -55
  71. pyavd/_eos_cli_config_gen/schema/eos_cli_config_gen.schema.pickle +0 -0
  72. pyavd/_eos_designs/eos_designs_facts/schema/eos_designs.schema.pickle +0 -0
  73. pyavd/_eos_designs/eos_designs_facts/schema/protocol.py +24 -20
  74. pyavd/_eos_designs/eos_designs_facts/uplinks.py +43 -32
  75. pyavd/_eos_designs/eos_designs_facts/vlans.py +123 -30
  76. pyavd/_eos_designs/schema/__init__.py +17122 -2922
  77. pyavd/_eos_designs/schema/eos_designs.schema.pickle +0 -0
  78. pyavd/_eos_designs/shared_utils/filtered_tenants.py +32 -19
  79. pyavd/_eos_designs/shared_utils/inband_management.py +7 -2
  80. pyavd/_eos_designs/shared_utils/mgmt.py +19 -10
  81. pyavd/_eos_designs/shared_utils/misc.py +93 -35
  82. pyavd/_eos_designs/shared_utils/node_type.py +4 -2
  83. pyavd/_eos_designs/shared_utils/utils.py +9 -2
  84. pyavd/_eos_designs/structured_config/base/__init__.py +45 -512
  85. pyavd/_eos_designs/structured_config/base/aaa_settings.py +196 -0
  86. pyavd/_eos_designs/structured_config/base/daemon_terminattr.py +2 -2
  87. pyavd/_eos_designs/structured_config/base/dns_settings.py +52 -0
  88. pyavd/_eos_designs/structured_config/base/dot1x.py +88 -0
  89. pyavd/_eos_designs/structured_config/base/logging.py +83 -0
  90. pyavd/_eos_designs/structured_config/base/management_interface.py +57 -0
  91. pyavd/_eos_designs/structured_config/base/monitor_connectivity.py +71 -0
  92. pyavd/_eos_designs/structured_config/base/monitor_sessions.py +41 -4
  93. pyavd/_eos_designs/structured_config/base/ptp.py +126 -0
  94. pyavd/_eos_designs/structured_config/base/router_bgp.py +66 -0
  95. pyavd/_eos_designs/structured_config/base/snmp_server.py +0 -12
  96. pyavd/_eos_designs/structured_config/base/utils.py +3 -3
  97. pyavd/_eos_designs/structured_config/connected_endpoints/__init__.py +2 -0
  98. pyavd/_eos_designs/structured_config/connected_endpoints/ethernet_interfaces.py +17 -2
  99. pyavd/_eos_designs/structured_config/connected_endpoints/mac_access_lists.py +88 -0
  100. pyavd/_eos_designs/structured_config/connected_endpoints/port_channel_interfaces.py +39 -8
  101. pyavd/_eos_designs/structured_config/connected_endpoints/utils.py +22 -1
  102. pyavd/_eos_designs/structured_config/constants.py +0 -16
  103. pyavd/_eos_designs/structured_config/core_interfaces_and_l3_edge/utils.py +2 -2
  104. pyavd/_eos_designs/structured_config/inband_management/__init__.py +104 -157
  105. pyavd/_eos_designs/structured_config/metadata/digital_twin.py +1 -3
  106. pyavd/_eos_designs/structured_config/mlag/__init__.py +2 -2
  107. pyavd/_eos_designs/structured_config/network_services/ip_access_lists.py +8 -0
  108. pyavd/_eos_designs/structured_config/network_services/router_bgp.py +6 -1
  109. pyavd/_eos_designs/structured_config/network_services/spanning_tree.py +7 -3
  110. pyavd/_eos_designs/structured_config/network_services/utils_zscaler.py +8 -2
  111. pyavd/_eos_designs/structured_config/network_services/vlan_interfaces.py +37 -0
  112. pyavd/_eos_designs/structured_config/network_services/vlans.py +1 -1
  113. pyavd/_eos_designs/structured_config/structured_config_utils/__init__.py +56 -0
  114. pyavd/_eos_designs/structured_config/structured_config_utils/mlag.py +93 -0
  115. pyavd/_eos_designs/structured_config/structured_config_utils/sflow.py +69 -0
  116. pyavd/_eos_designs/structured_config/structured_config_utils/underlay.py +124 -0
  117. pyavd/_eos_designs/structured_config/structured_config_utils/utils.py +44 -0
  118. pyavd/_eos_designs/structured_config/underlay/dhcp_servers.py +2 -2
  119. pyavd/_eos_designs/structured_config/underlay/ethernet_interfaces.py +4 -0
  120. pyavd/_eos_designs/structured_config/underlay/loopback_interfaces.py +30 -6
  121. pyavd/_eos_designs/structured_config/underlay/port_channel_interfaces.py +3 -0
  122. pyavd/_eos_designs/structured_config/underlay/router_isis.py +10 -6
  123. pyavd/_eos_designs/structured_config/underlay/utils.py +4 -3
  124. pyavd/_schema/avd_meta_schema.pickle +0 -0
  125. pyavd/_schema/schemas.json.gz +0 -0
  126. pyavd/_utils/password_utils/password.py +0 -38
  127. pyavd/api/fabric_documentation/__init__.py +1 -1
  128. pyavd/api/interface_descriptions/__init__.py +18 -0
  129. pyavd/get_fabric_documentation.py +2 -1
  130. {pyavd-6.2.0.dev2.dist-info → pyavd-6.3.0.dev1.dist-info}/METADATA +3 -3
  131. {pyavd-6.2.0.dev2.dist-info → pyavd-6.3.0.dev1.dist-info}/RECORD +134 -119
  132. pyavd/_eos_designs/structured_config/structured_config_utils.py +0 -317
  133. {pyavd-6.2.0.dev2.dist-info → pyavd-6.3.0.dev1.dist-info}/WHEEL +0 -0
  134. {pyavd-6.2.0.dev2.dist-info → pyavd-6.3.0.dev1.dist-info}/licenses/pyavd/LICENSE +0 -0
  135. {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.2.0.dev2"
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
- yield VerifyTransceiversManufacturers.Input(manufacturers=list(self.structured_config.metadata.validate_hardware.transceiver_manufacturers))
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 the sending of emails on Azure smtp server
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 an Azure smtp server
1092
- uses auth_username
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 entity type name to entity
582
- The possible keys to this map are ENTITY_TYPE_STUDIO,
583
- ENTITY_TYPE_INPUTS, ENTITY_TYPE_ASSIGNED_TAGS,
584
- ENTITY_TYPE_BUILD_HOOK and ENTITY_TYPE_AUTOFILL_ACTION.
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
- `{ fieldId: floatField1ID }`,
935
- `{ fieldId: floatField2ID }`
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
- users = [\{\"id\":\"u1\",\"name\":\"Alice\"\}]
1395
- key_path = [\"users\", \"[id=u1]\", \"name\"]
1396
- path = [\"users\", \"0\", \"name\"]
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
- studio, inputs, assigned tags: [“studio_id”, <id>]
1422
- buildhook: [“studio_id”, <id>, “hook_id”, <id>]
1423
- autofill: [“studio_id”, <id>, “input_field_id”, <id>]
1424
- configlet: [“configlet_id”, <id>]
1425
- configletassignment : [“configlet_assignment_id”, <id>]
1426
- tags:
1427
- element_type is one of \"1\" (device), \"2\" (interface)
1428
- [\"creator_type\", \"2\", \"element_type\", <element_type>,
1429
- \"element_sub_type\", \"1\", \"label\", <label>, \"value\", <value>]
1430
- tag assignments:
1431
- For element_type = \"1\" (device)
1432
- [\"creator_type\", \"2\", \"element_type\", \"1\", \"element_sub_type\", \"1\",
1433
- \"label\", <label>, \"value\", <value>, \"device_id\", <id>]
1434
- For element_type = \"2\" (interface)
1435
- [\"creator_type\", \"2\", \"element_type\", \"2\", \"element_sub_type\", \"1\",
1436
- \"label\", <label>, \"value\", <value>, \"device_id\", <id>,
1437
- \"interface_id\", <id>]
1438
- hierarchy related entities:
1439
- node: [\"node_id\", <id>]
1440
- fixture_class: [\"fixture_class_id\", <id>]
1441
- fixture_instance: [\"fixture_instance_id\", <id>]
1442
- processor: [\"processor_id\", <id>]
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
 
@@ -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(ssl_context)
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=ssl_context)
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
- def _ssl_context(self) -> ssl.SSLContext | bool:
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
- Initialize the default SSL context with relaxed verification if needed.
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
- Otherwise we just return True.
134
- The return value (The default ssl context or True) will be passed to grpclib.
135
- Requests will pick it up from ssl lib itself.
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
- else:
144
- context = True
145
- return context
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._verify_certs,
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._verify_certs,
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
+ )
@@ -89,3 +89,7 @@ class CVClientBulkAPIError(CVClientException):
89
89
 
90
90
  class CVGRPCError(CVClientException):
91
91
  """GRPC call failed."""
92
+
93
+
94
+ class CVClientInvalidServerName(CVClientException):
95
+ """CloudVision server FQDN is invalid."""
@@ -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."""