-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[NDM] Add NDM metadata support for Cisco ACI #17735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
eece332
2dc6b77
0fbd388
308381e
aaaf282
d7e3d76
df7ccfa
2874add
79954aa
3c84f1c
8dd93c7
7d7c5a4
ac9fe08
7848a1c
3457360
3f3cd66
2b4ee06
c6c20f9
6137d1f
502e682
f51307d
ff002e5
eb0ec6b
63e2f08
6d8c377
04a4d2e
a93dfa7
86c4fea
ddcd8d7
aeacd2e
0c58f52
2a8f97e
c9f81a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[NDM] Add NDM metadata support for Cisco ACI |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -2,21 +2,34 @@ | |||||
# All rights reserved | ||||||
# Licensed under a 3-clause BSD style license (see LICENSE) | ||||||
|
||||||
from six import iteritems | ||||||
from six import PY3, iteritems | ||||||
|
||||||
from datadog_checks.base.utils.serialization import json | ||||||
|
||||||
if PY3: | ||||||
from datadog_checks.cisco_aci.models import DeviceMetadata, Eth, InterfaceMetadata, Node | ||||||
else: | ||||||
DeviceMetadata = None | ||||||
Eth = None | ||||||
InterfaceMetadata = None | ||||||
Node = None | ||||||
|
||||||
from . import aci_metrics, exceptions, helpers | ||||||
|
||||||
VENDOR_CISCO = 'cisco' | ||||||
|
||||||
|
||||||
class Fabric: | ||||||
""" | ||||||
Collect fabric metrics from the APIC | ||||||
""" | ||||||
|
||||||
def __init__(self, check, api, instance): | ||||||
def __init__(self, check, api, instance, namespace): | ||||||
self.check = check | ||||||
self.api = api | ||||||
self.instance = instance | ||||||
self.check_tags = check.check_tags | ||||||
self.namespace = namespace | ||||||
|
||||||
# grab some functions from the check | ||||||
self.gauge = check.gauge | ||||||
|
@@ -25,6 +38,7 @@ def __init__(self, check, api, instance): | |||||
self.submit_metrics = check.submit_metrics | ||||||
self.tagger = self.check.tagger | ||||||
self.external_host_tags = self.check.external_host_tags | ||||||
self.ndm_metadata = check.ndm_metadata | ||||||
|
||||||
def collect(self): | ||||||
fabric_pods = self.api.get_fabric_pods() | ||||||
|
@@ -70,6 +84,8 @@ def submit_nodes_health(self, nodes, pods): | |||||
continue | ||||||
self.log.info("processing node %s on pod %s", node_id, pod_id) | ||||||
try: | ||||||
if PY3: | ||||||
self.submit_node_metadata(node_attrs, tags) | ||||||
self.submit_process_metric(n, tags + self.check_tags + user_tags, hostname=hostname) | ||||||
except (exceptions.APIConnectionException, exceptions.APIParsingException): | ||||||
pass | ||||||
|
@@ -94,6 +110,8 @@ def process_eth(self, node): | |||||
eth_attrs = helpers.get_attributes(e) | ||||||
eth_id = eth_attrs['id'] | ||||||
tags = self.tagger.get_fabric_tags(e, 'l1PhysIf') | ||||||
if PY3: | ||||||
self.submit_interface_metadata(eth_attrs, node['address'], tags) | ||||||
try: | ||||||
stats = self.api.get_eth_stats(pod_id, node['id'], eth_id) | ||||||
self.submit_fabric_metric(stats, tags, 'l1PhysIf', hostname=hostname) | ||||||
|
@@ -209,3 +227,43 @@ def get_fabric_type(self, obj_type): | |||||
return 'pod' | ||||||
if obj_type == 'l1PhysIf': | ||||||
return 'port' | ||||||
|
||||||
def submit_node_metadata(self, node_attrs, tags): | ||||||
node = Node(attributes=node_attrs) | ||||||
id_tags = ['namespace:{}'.format(self.namespace), 'system_ip:{}'.format(node.attributes.address)] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Do we add the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i have a separate ticket for that work ! i'm doing that in the bg, but if you prefer it all in one PR i can amend that! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good for me to add it separately 👍 i think here we just want to ensure that the id tags can be used to filter metrics down to a specific device (so |
||||||
device_tags = [ | ||||||
'device_vendor:{}'.format(VENDOR_CISCO), | ||||||
'device_namespace:{}'.format(self.namespace), | ||||||
'device_hostname:{}'.format(node.attributes.dn), | ||||||
'hostname:{}'.format(node.attributes.dn), | ||||||
'system_ip:{}'.format(node.attributes.address), | ||||||
zoedt marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
'device_ip:{}'.format(node.attributes.address), | ||||||
'id:{}:{}'.format(self.namespace, node.attributes.address), | ||||||
] | ||||||
device = DeviceMetadata( | ||||||
id='{}:{}'.format(self.namespace, node.attributes.address), | ||||||
id_tags=id_tags, | ||||||
tags=device_tags + tags, | ||||||
name=node.attributes.dn, | ||||||
ip_address=node.attributes.address, | ||||||
model=node.attributes.model, | ||||||
fabric_st=node.attributes.fabric_st, | ||||||
vendor=VENDOR_CISCO, | ||||||
version=node.attributes.version, | ||||||
serial_number=node.attributes.serial, | ||||||
device_type=node.attributes.device_type, | ||||||
) | ||||||
self.ndm_metadata(json.dumps(device.model_dump())) | ||||||
|
||||||
def submit_interface_metadata(self, eth_attr, address, tags): | ||||||
eth = Eth(attributes=eth_attr) | ||||||
interface = InterfaceMetadata( | ||||||
device_id='{}:{}'.format(self.namespace, address), | ||||||
id_tags=tags, | ||||||
zoedt marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
index=eth.attributes.id, | ||||||
name=eth.attributes.name, | ||||||
description=eth.attributes.desc, | ||||||
mac_address=eth.attributes.router_mac, | ||||||
admin_status=eth.attributes.admin_st, | ||||||
) | ||||||
self.ndm_metadata(json.dumps(interface.model_dump(exclude_none=True))) | ||||||
zoedt marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# (C) Datadog, Inc. 2024-present | ||
# All rights reserved | ||
# Licensed under a 3-clause BSD style license (see LICENSE) | ||
|
||
import six | ||
|
||
if six.PY3: | ||
from typing import Optional | ||
|
||
from pydantic import BaseModel, Field, computed_field, field_validator | ||
|
||
class NodeAttributes(BaseModel): | ||
address: Optional[str] = None | ||
fabric_st: Optional[str] = Field(default=None, alias="fabricSt") | ||
role: Optional[str] = None | ||
dn: Optional[str] = None | ||
model: Optional[str] = None | ||
version: Optional[str] = None | ||
serial: Optional[str] = None | ||
vendor: Optional[str] = Field(default='cisco-aci') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is just parsing the info from the response we get back from the cisco aci api response - but for example i'm referring to with sd-wan here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. d'oh, just saw |
||
namespace: Optional[str] = Field(default='default') | ||
|
||
@computed_field | ||
@property | ||
def device_type(self) -> str: | ||
if self.role in ['leaf', 'spine']: | ||
return 'switch' | ||
if self.role in ['controller', 'vleaf', 'vip', 'protection-chain']: | ||
zoedt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return 'cisco_aci' | ||
return 'other' | ||
|
||
class Node(BaseModel): | ||
attributes: NodeAttributes | ||
|
||
class EthAttributes(BaseModel): | ||
admin_st: Optional[str] = Field(default=None, alias="adminSt") | ||
id: Optional[str] = None | ||
name: Optional[str] = None | ||
desc: Optional[str] = None | ||
router_mac: Optional[str] = Field(default=None, alias="routerMac") | ||
|
||
class Eth(BaseModel): | ||
attributes: EthAttributes | ||
|
||
class DeviceMetadata(BaseModel): | ||
zoedt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
id: Optional[str] = Field(default=None) | ||
id_tags: list = Field(default_factory=list) | ||
tags: list = Field(default_factory=list) | ||
name: Optional[str] = Field(default=None) | ||
ip_address: Optional[str] = Field(default=None) | ||
model: Optional[str] = Field(default=None) | ||
fabric_st: Optional[str] = Field(default=None, exclude=True) | ||
vendor: Optional[str] = Field(default=None) | ||
version: Optional[str] = Field(default=None) | ||
serial_number: Optional[str] = Field(default=None) | ||
device_type: Optional[str] = Field(default=None) | ||
integration: Optional[str] = Field(default='cisco-aci') | ||
|
||
@computed_field | ||
@property | ||
def status(self) -> int: | ||
mapping = { | ||
'active': 1, | ||
'inactive': 2, | ||
'disabled': 5, | ||
'discovering': 2, | ||
'undiscovered': 2, | ||
'unsupported': 2, | ||
'unknown': 4, | ||
} | ||
return mapping.get(self.fabric_st, 7) | ||
zoedt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
class DeviceMetadataList(BaseModel): | ||
device_metadata: list = Field(default_factory=list) | ||
|
||
class InterfaceMetadata(BaseModel): | ||
device_id: Optional[str] = Field(default=None) | ||
id_tags: list = Field(default_factory=list) | ||
index: Optional[str] = Field(default=None) | ||
name: Optional[str] = Field(default=None) | ||
description: Optional[str] = Field(default=None) | ||
mac_address: Optional[str] = Field(default=None) | ||
admin_status: Optional[int] = Field(default=None) | ||
oper_status: Optional[int] = Field(default=None) | ||
|
||
@field_validator("admin_status", mode="before") | ||
@classmethod | ||
def parse_admin_status(cls, admin_status: int | None) -> int | None: | ||
if not admin_status: | ||
return None | ||
|
||
if admin_status == "up": | ||
return 1 | ||
return 2 | ||
|
||
@field_validator("oper_status", mode="before") | ||
@classmethod | ||
def parse_oper_status(cls, oper_status: int | None) -> int | None: | ||
if not oper_status: | ||
return None | ||
|
||
if oper_status == "up": | ||
return 1 | ||
return 2 | ||
|
||
class InterfaceMetadataList(BaseModel): | ||
interface_metadata: list = Field(default_factory=list) | ||
|
||
class NetworkDevicesMetadata(BaseModel): | ||
subnet: Optional[str] = None | ||
namespace: str = None | ||
devices: Optional[list[DeviceMetadata]] = Field(default_factory=list) | ||
interfaces: Optional[list[InterfaceMetadata]] = Field(default_factory=list) | ||
collect_timestamp: Optional[float] = None |
Uh oh!
There was an error while loading. Please reload this page.