Skip to content

Commit 29bab07

Browse files
NouemanKHALclaude
andauthored
Add hostname_transform config option to Nutanix integration (DataDog#23769)
* feat(nutanix): add hostname_transform config option to spec Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * feat(nutanix): apply hostname_transform in infrastructure monitor Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * test(nutanix): add tests for hostname_transform option Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * test(nutanix): fix false-positive hostname_transform tests Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * chore(nutanix): add changelog entry for hostname_transform * fix ddev validate * Use enum type for hostname_transform config option Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ddev validate models -s * Restore original description for hostname_transform * fix(nutanix): use display_hostname in stats exception log --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 22e54b1 commit 29bab07

8 files changed

Lines changed: 121 additions & 10 deletions

File tree

nutanix/assets/configuration/spec.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,20 @@ files:
229229
type: boolean
230230
example: true
231231
fleet_configurable: false
232+
- name: hostname_transform
233+
description: |
234+
Should be either 'upper' or 'lower' or 'default'.
235+
If 'upper', the check will convert the Nutanix hostname to uppercase.
236+
If 'lower', the check will convert the Nutanix hostname to lowercase.
237+
If 'default', the check will not transform the Nutanix hostname (the default option).
238+
Use this if you install the agent in VMs so that the hosts are not duplicated in the web application UI.
239+
fleet_configurable: false
240+
value:
241+
type: string
242+
enum:
243+
- upper
244+
- lower
245+
- default
232246
- template: instances/http
233247
- template: instances/default
234248
overrides:

nutanix/changelog.d/23769.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `hostname_transform` config option to transform all Nutanix hostnames (hosts and VMs) to uppercase, lowercase, or leave them unchanged.

nutanix/datadog_checks/nutanix/config_models/instance.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class InstanceConfig(BaseModel):
9090
exclude_filtered_resources_from_cluster_capacity: Optional[bool] = None
9191
extra_headers: Optional[MappingProxyType[str, Any]] = None
9292
headers: Optional[MappingProxyType[str, Any]] = None
93+
hostname_transform: Optional[Literal['upper', 'lower', 'default']] = None
9394
kerberos_auth: Optional[Literal['required', 'optional', 'disabled']] = None
9495
kerberos_cache: Optional[str] = None
9596
kerberos_delegate: Optional[bool] = None

nutanix/datadog_checks/nutanix/data/conf.yaml.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@ instances:
186186
#
187187
# batch_vm_collection: true
188188

189+
## @param hostname_transform - string - optional
190+
## Should be either 'upper' or 'lower' or 'default'.
191+
## If 'upper', the check will convert the Nutanix hostname to uppercase.
192+
## If 'lower', the check will convert the Nutanix hostname to lowercase.
193+
## If 'default', the check will not transform the Nutanix hostname (the default option).
194+
## Use this if you install the agent in VMs so that the hosts are not duplicated in the web application UI.
195+
#
196+
# hostname_transform: <HOSTNAME_TRANSFORM>
197+
189198
## @param proxy - mapping - optional
190199
## This overrides the `proxy` setting in `init_config`.
191200
##

nutanix/datadog_checks/nutanix/infrastructure_monitor.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,12 @@ def _process_vm(self, vm: dict, vm_stats_dict: dict[str, list[dict]], cluster_na
237237
if not self._should_collect_vm(vm):
238238
return False
239239

240+
display_hostname = self._transform_hostname(vm_name)
241+
240242
vm_tags = self.check.base_tags + self._extract_vm_tags(vm)
241-
self._set_external_tags_for_host(vm_name, vm_tags)
242-
self._report_vm_basic_metrics(vm, vm_name, vm_tags)
243-
self._report_vm_stats(vm_id, vm_name, vm_tags, vm_stats_dict, cluster_name)
243+
self._set_external_tags_for_host(display_hostname, vm_tags)
244+
self._report_vm_basic_metrics(vm, display_hostname, vm_tags)
245+
self._report_vm_stats(vm_id, display_hostname, vm_tags, vm_stats_dict, cluster_name)
244246
return True
245247

246248
def _report_vm_basic_metrics(self, vm: dict, vm_name: str, vm_tags: list[str]) -> None:
@@ -435,11 +437,13 @@ def _process_single_host(
435437
self.host_count += 1
436438
self.host_names[host_id] = host_name
437439

440+
display_hostname = self._transform_hostname(host_name)
441+
438442
host_tags = cluster_tags + self._extract_host_tags(host)
439-
self.check.gauge("host.count", 1, hostname=host_name, tags=host_tags)
440-
self._report_host_status_metrics(host, host_name, host_tags)
441-
self._set_external_tags_for_host(host_name, host_tags)
442-
self._report_host_capacity_metrics(host, host_name, host_tags)
443+
self.check.gauge("host.count", 1, hostname=display_hostname, tags=host_tags)
444+
self._report_host_status_metrics(host, display_hostname, host_tags)
445+
self._set_external_tags_for_host(display_hostname, host_tags)
446+
self._report_host_capacity_metrics(host, display_hostname, host_tags)
443447

444448
try:
445449
stats = self._get_stats(f"api/clustermgmt/v4.0/stats/clusters/{cluster_id}/hosts/{host_id}")
@@ -449,12 +453,12 @@ def _process_single_host(
449453
stats,
450454
HOST_STATS_METRICS,
451455
host_tags,
452-
hostname=host_name,
456+
hostname=display_hostname,
453457
extra_tags_by_key=self._get_disk_status_storage_tags(host_id),
454458
)
455459
except Exception:
456460
self.check.log.exception(
457-
"[%s][%s] Failed to fetch stats for host %s", self._pc_label, cluster_name, host_name
461+
"[%s][%s] Failed to fetch stats for host %s", self._pc_label, cluster_name, display_hostname
458462
)
459463

460464
host_label = host_name or host_id
@@ -573,6 +577,17 @@ def _set_external_tags_for_host(self, hostname: str, tags: list[str]) -> None:
573577

574578
self.external_tags.append((hostname, {self.check.__NAMESPACE__: tags}))
575579

580+
def _transform_hostname(self, hostname: str | None) -> str | None:
581+
"""Apply hostname_transform config to a hostname."""
582+
if not hostname:
583+
return hostname
584+
transform = self.check.config.hostname_transform
585+
if transform == 'upper':
586+
return hostname.upper()
587+
if transform == 'lower':
588+
return hostname.lower()
589+
return hostname
590+
576591
def _should_collect_vm(self, vm: dict) -> bool:
577592
"""Check if a VM should be collected based on power state and resource filters."""
578593
has_power_state_filter = any(

nutanix/tests/test_configuration.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,12 @@ def test_category_tags_with_prefix_for_system_and_user_types(dd_run_check, aggre
110110
category_tags = [t for t in metric.tags if "Environment:" in t or "Team:" in t]
111111
unprefixed_tags = [t for t in category_tags if not t.startswith("ntnx_")]
112112
assert not unprefixed_tags, f"Category tags must have ntnx_ prefix, found without: {unprefixed_tags}"
113+
114+
115+
def test_invalid_hostname_transform_raises_error(dd_run_check, mock_instance):
116+
instance = mock_instance.copy()
117+
instance['hostname_transform'] = 'INVALID'
118+
check = NutanixCheck('nutanix', {}, [instance])
119+
120+
with pytest.raises(Exception, match="hostname_transform"):
121+
dd_run_check(check)

nutanix/tests/test_hosts.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,47 @@ def test_external_tags_for_host(dd_run_check, aggregator, mock_instance, mock_ht
102102
HOST_NAME,
103103
{'nutanix': HOST_TAGS},
104104
)
105+
106+
107+
def test_hostname_transform_upper_applies_to_host(dd_run_check, aggregator, mock_instance, mock_http_get):
108+
instance = mock_instance.copy()
109+
instance['hostname_transform'] = 'upper'
110+
check = NutanixCheck('nutanix', {}, [instance])
111+
dd_run_check(check)
112+
113+
aggregator.assert_metric("nutanix.host.count", value=1, tags=HOST_TAGS, hostname=HOST_NAME.upper())
114+
115+
116+
def test_hostname_transform_lower_applies_to_host(dd_run_check, aggregator, mock_instance, mock_http_get, mocker):
117+
cluster_id = "00064715-c043-5d8f-ee4b-176ec875554d"
118+
uppercase_host = {
119+
"extId": "d8787814-4fe8-4ba5-931f-e1ee31c294a6",
120+
"hostName": "UPPER-HOST-10-0-0-103",
121+
"hostType": "HYPER_CONVERGED",
122+
"maintenanceState": "NORMAL",
123+
"hypervisor": {
124+
"type": "AHV",
125+
"acropolisConnectionState": "CONNECTED",
126+
"fullName": "AHV 10.3",
127+
},
128+
"nodeStatus": "NORMAL",
129+
}
130+
mocker.patch(
131+
"datadog_checks.nutanix.infrastructure_monitor.InfrastructureMonitor._list_hosts_by_cluster",
132+
side_effect=lambda cid: [uppercase_host] if cid == cluster_id else [],
133+
)
134+
instance = mock_instance.copy()
135+
instance['hostname_transform'] = 'lower'
136+
check = NutanixCheck('nutanix', {}, [instance])
137+
dd_run_check(check)
138+
139+
aggregator.assert_metric("nutanix.host.count", value=1, hostname="upper-host-10-0-0-103")
140+
141+
142+
def test_hostname_transform_default_leaves_host_unchanged(dd_run_check, aggregator, mock_instance, mock_http_get):
143+
instance = mock_instance.copy()
144+
instance['hostname_transform'] = 'default'
145+
check = NutanixCheck('nutanix', {}, [instance])
146+
dd_run_check(check)
147+
148+
aggregator.assert_metric("nutanix.host.count", value=1, tags=HOST_TAGS, hostname=HOST_NAME)

nutanix/tests/test_vms.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from datadog_checks.nutanix import NutanixCheck
1010
from tests.conftest import load_fixture_page
11-
from tests.constants import OFF_VM_NAME, OFF_VM_TAGS, PCVM_NAME, PCVM_TAGS
11+
from tests.constants import OFF_VM_NAME, OFF_VM_TAGS, PCVM_NAME, PCVM_TAGS, UBUNTU_VM_NAME, UBUNTU_VM_TAGS
1212
from tests.metrics import VM_STATS_METRICS_REQUIRED
1313

1414
pytestmark = [pytest.mark.unit]
@@ -170,3 +170,21 @@ def test_external_tags_for_vm(dd_run_check, aggregator, mock_instance, mock_http
170170
PCVM_NAME,
171171
{'nutanix': PCVM_TAGS},
172172
)
173+
174+
175+
def test_hostname_transform_upper_applies_to_vm(dd_run_check, aggregator, mock_instance, mock_http_get):
176+
instance = mock_instance.copy()
177+
instance['hostname_transform'] = 'upper'
178+
check = NutanixCheck('nutanix', {}, [instance])
179+
dd_run_check(check)
180+
181+
aggregator.assert_metric("nutanix.vm.count", value=1, tags=UBUNTU_VM_TAGS, hostname=UBUNTU_VM_NAME.upper())
182+
183+
184+
def test_hostname_transform_lower_applies_to_vm(dd_run_check, aggregator, mock_instance, mock_http_get):
185+
instance = mock_instance.copy()
186+
instance['hostname_transform'] = 'lower'
187+
check = NutanixCheck('nutanix', {}, [instance])
188+
dd_run_check(check)
189+
190+
aggregator.assert_metric("nutanix.vm.count", value=1, tags=PCVM_TAGS, hostname=PCVM_NAME.lower())

0 commit comments

Comments
 (0)