Skip to content

Commit b982fac

Browse files
simathihCopilot
andauthored
Fix duplicate config filenames and add unit tests with CI (#2185)
- Fix telegraf config filename collision: include plugin name in filename to prevent plugins in the same omiclass from overwriting each other (e.g. mem/swap/kernel_vmstat under 'memory') - Add comprehensive unit tests for all LAD-AMA-Common modules (91 tests) - Add GitHub Actions CI workflow triggered on LAD-AMA-Common changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fbeacd8 commit b982fac

8 files changed

Lines changed: 923 additions & 1 deletion
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: LAD-AMA-Common Tests
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
paths:
7+
- 'LAD-AMA-Common/**'
8+
pull_request:
9+
branches: [master, main]
10+
paths:
11+
- 'LAD-AMA-Common/**'
12+
13+
jobs:
14+
test:
15+
runs-on: ubuntu-latest
16+
strategy:
17+
matrix:
18+
python-version: ['3.9', '3.10', '3.11', '3.12']
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Set up Python ${{ matrix.python-version }}
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
28+
- name: Run tests
29+
working-directory: LAD-AMA-Common
30+
run: python -m unittest discover -s tests -v

LAD-AMA-Common/telegraf_utils/telegraf_config_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def parse_config(data, me_url, mdsd_url, is_lad, az_resource_id, subscription_id
179179
all_config_ids.append(configId)
180180

181181
for configId in all_config_ids:
182-
config_file = {"filename" : omiclass+"-"+configId+".conf"}
182+
config_file = {"filename" : omiclass+"-"+plugin+"-"+configId+".conf"}
183183
input_str = ""
184184
ama_rename_str = ""
185185
metricsext_rename_str = ""

LAD-AMA-Common/tests/__init__.py

Whitespace-only changes.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env python
2+
"""
3+
Unit tests for metrics_ext_utils/metrics_common_utils.py
4+
"""
5+
6+
import sys
7+
import os
8+
import unittest
9+
from unittest.mock import patch
10+
11+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
12+
13+
from metrics_ext_utils.metrics_common_utils import is_systemd, is_arc_installed, get_arc_endpoint
14+
15+
16+
class TestIsSystemd(unittest.TestCase):
17+
"""Tests for is_systemd function."""
18+
19+
@patch('os.path.isdir')
20+
def test_returns_true_when_systemd_dir_exists(self, mock_isdir):
21+
mock_isdir.return_value = True
22+
self.assertTrue(is_systemd())
23+
mock_isdir.assert_called_with("/run/systemd/system")
24+
25+
@patch('os.path.isdir')
26+
def test_returns_false_when_systemd_dir_missing(self, mock_isdir):
27+
mock_isdir.return_value = False
28+
self.assertFalse(is_systemd())
29+
30+
31+
class TestIsArcInstalled(unittest.TestCase):
32+
"""Tests for is_arc_installed function."""
33+
34+
@patch('os.system')
35+
def test_returns_true_when_himdsd_running(self, mock_system):
36+
mock_system.return_value = 0
37+
self.assertTrue(is_arc_installed())
38+
mock_system.assert_called_with("systemctl status himdsd 1>/dev/null 2>&1")
39+
40+
@patch('os.system')
41+
def test_returns_false_when_himdsd_not_running(self, mock_system):
42+
mock_system.return_value = 3 # systemctl returns non-zero for inactive
43+
self.assertFalse(is_arc_installed())
44+
45+
46+
class TestGetArcEndpoint(unittest.TestCase):
47+
"""Tests for get_arc_endpoint function."""
48+
49+
@patch('builtins.open', unittest.mock.mock_open(
50+
read_data='DefaultEnvironment="IMDS_ENDPOINT=http://localhost:40342"\n'))
51+
def test_parses_endpoint_from_conf(self):
52+
endpoint = get_arc_endpoint()
53+
self.assertEqual(endpoint, "http://localhost:40342")
54+
55+
@patch('builtins.open', unittest.mock.mock_open(
56+
read_data='DefaultEnvironment="IMDS_ENDPOINT=http://10.0.0.1:40342"\nOTHER="value"\n'))
57+
def test_parses_custom_endpoint(self):
58+
endpoint = get_arc_endpoint()
59+
self.assertEqual(endpoint, "http://10.0.0.1:40342")
60+
61+
62+
if __name__ == '__main__':
63+
unittest.main()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env python
2+
"""
3+
Unit tests for metrics_ext_utils/metrics_constants.py
4+
"""
5+
6+
import sys
7+
import os
8+
import unittest
9+
10+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
11+
12+
import metrics_ext_utils.metrics_constants as metrics_constants
13+
14+
15+
class TestMetricsConstants(unittest.TestCase):
16+
"""Tests for metrics_constants module values."""
17+
18+
def test_namespace_value(self):
19+
self.assertEqual(metrics_constants.metrics_extension_namespace,
20+
"Azure.VM.Linux.GuestMetrics")
21+
22+
def test_ama_binary_path(self):
23+
self.assertEqual(metrics_constants.ama_metrics_extension_bin,
24+
"/opt/microsoft/azuremonitoragent/bin/MetricsExtension")
25+
26+
def test_lad_binary_path(self):
27+
self.assertEqual(metrics_constants.lad_metrics_extension_bin,
28+
"/usr/local/lad/bin/MetricsExtension")
29+
30+
def test_ama_telegraf_bin(self):
31+
self.assertEqual(metrics_constants.ama_telegraf_bin,
32+
"/opt/microsoft/azuremonitoragent/bin/telegraf")
33+
34+
def test_lad_telegraf_bin(self):
35+
self.assertEqual(metrics_constants.lad_telegraf_bin,
36+
"/usr/local/lad/bin/telegraf")
37+
38+
def test_service_names(self):
39+
self.assertEqual(metrics_constants.metrics_extension_service_name, "metrics-extension")
40+
self.assertEqual(metrics_constants.telegraf_service_name, "metrics-sourcer")
41+
self.assertEqual(metrics_constants.lad_metrics_extension_service_name, "metrics-extension-lad")
42+
self.assertEqual(metrics_constants.lad_telegraf_service_name, "metrics-sourcer-lad")
43+
44+
def test_udp_ports_are_strings(self):
45+
self.assertIsInstance(metrics_constants.ama_metrics_extension_udp_port, str)
46+
self.assertIsInstance(metrics_constants.lad_metrics_extension_udp_port, str)
47+
48+
def test_influx_url_format(self):
49+
self.assertTrue(
50+
metrics_constants.lad_metrics_extension_influx_udp_url.startswith("udp://"))
51+
self.assertIn(metrics_constants.lad_metrics_extension_udp_port,
52+
metrics_constants.lad_metrics_extension_influx_udp_url)
53+
54+
def test_telegraf_influx_url(self):
55+
self.assertTrue(
56+
metrics_constants.telegraf_influx_url.startswith("unix://"))
57+
58+
59+
if __name__ == '__main__':
60+
unittest.main()
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#!/usr/bin/env python
2+
"""
3+
Unit tests for metrics_ext_utils/metrics_ext_handler.py
4+
"""
5+
6+
import sys
7+
import os
8+
import json
9+
import unittest
10+
from unittest.mock import patch, MagicMock, mock_open
11+
12+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
13+
14+
# Mock Linux-only modules before importing the handler
15+
for mod_name in ('grp', 'pwd'):
16+
if mod_name not in sys.modules:
17+
sys.modules[mod_name] = MagicMock()
18+
19+
import metrics_ext_utils.metrics_constants as metrics_constants
20+
from metrics_ext_utils.metrics_ext_handler import (
21+
create_metrics_extension_conf,
22+
create_custom_metrics_conf,
23+
get_metrics_extension_service_name,
24+
get_arm_domain,
25+
ARMDomainMap,
26+
PublicCloudName,
27+
FairfaxCloudName,
28+
MooncakeCloudName,
29+
USNatCloudName,
30+
USSecCloudName,
31+
)
32+
33+
34+
class TestCreateMetricsExtensionConf(unittest.TestCase):
35+
"""Tests for create_metrics_extension_conf."""
36+
37+
def test_contains_resource_id(self):
38+
resource_id = "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1"
39+
conf = create_metrics_extension_conf(resource_id, "https://login.microsoftonline.com/tenant1")
40+
conf_json = json.loads(conf)
41+
self.assertEqual(conf_json["azureResourceId"], resource_id)
42+
43+
def test_contains_aad_authority(self):
44+
aad_url = "https://login.microsoftonline.com/tenant1"
45+
conf = create_metrics_extension_conf("/sub/rg/vm1", aad_url)
46+
conf_json = json.loads(conf)
47+
self.assertEqual(conf_json["aadAuthority"], aad_url)
48+
49+
def test_valid_json(self):
50+
conf = create_metrics_extension_conf("/some/id", "https://aad.url")
51+
conf_json = json.loads(conf)
52+
self.assertIn("timeToTerminateInMs", conf_json)
53+
self.assertIn("maxPublicationMetricsPerMinute", conf_json)
54+
self.assertEqual(conf_json["publicationIntervalInSec"], 60)
55+
56+
def test_publish_min_max_default_true(self):
57+
conf = create_metrics_extension_conf("/id", "https://aad")
58+
conf_json = json.loads(conf)
59+
self.assertTrue(conf_json["publishMinMaxByDefault"])
60+
61+
62+
class TestCreateCustomMetricsConf(unittest.TestCase):
63+
"""Tests for create_custom_metrics_conf."""
64+
65+
def test_default_gig_endpoint(self):
66+
conf = create_custom_metrics_conf("westus2")
67+
conf_json = json.loads(conf)
68+
self.assertEqual(conf_json["homeStampGslbHostname"], "westus2.monitoring.azure.com")
69+
self.assertIn("https://westus2.monitoring.azure.com/api/v1/ingestion/ingest",
70+
conf_json["endpointsForClientPublication"])
71+
72+
def test_custom_gig_endpoint(self):
73+
custom_ep = "https://custom.endpoint.example.com"
74+
conf = create_custom_metrics_conf("eastus", gig_endpoint=custom_ep)
75+
conf_json = json.loads(conf)
76+
self.assertEqual(conf_json["homeStampGslbHostname"], "custom.endpoint.example.com")
77+
self.assertIn(custom_ep + "/api/v1/ingestion/ingest",
78+
conf_json["endpointsForClientPublication"])
79+
80+
def test_valid_json_structure(self):
81+
conf = create_custom_metrics_conf("centralus")
82+
conf_json = json.loads(conf)
83+
self.assertIn("version", conf_json)
84+
self.assertEqual(conf_json["version"], 17)
85+
86+
87+
class TestGetMetricsExtensionServiceName(unittest.TestCase):
88+
"""Tests for get_metrics_extension_service_name."""
89+
90+
def test_lad_service_name(self):
91+
name = get_metrics_extension_service_name(True)
92+
self.assertEqual(name, metrics_constants.lad_metrics_extension_service_name)
93+
94+
def test_ama_service_name(self):
95+
name = get_metrics_extension_service_name(False)
96+
self.assertEqual(name, metrics_constants.metrics_extension_service_name)
97+
98+
99+
class TestGetArmDomain(unittest.TestCase):
100+
"""Tests for get_arm_domain."""
101+
102+
def test_public_cloud(self):
103+
domain = get_arm_domain("AzurePublicCloud")
104+
self.assertEqual(domain, "management.azure.com")
105+
106+
def test_fairfax_cloud(self):
107+
domain = get_arm_domain("AzureUSGovernmentCloud")
108+
self.assertEqual(domain, "management.usgovcloudapi.net")
109+
110+
def test_mooncake_cloud(self):
111+
domain = get_arm_domain("AzureChinaCloud")
112+
self.assertEqual(domain, "management.chinacloudapi.cn")
113+
114+
def test_usnat_cloud(self):
115+
domain = get_arm_domain("USNat")
116+
self.assertEqual(domain, "management.azure.eaglex.ic.gov")
117+
118+
def test_ussec_cloud(self):
119+
domain = get_arm_domain("USSec")
120+
self.assertEqual(domain, "management.azure.microsoft.scloud")
121+
122+
def test_unknown_cloud_raises(self):
123+
with self.assertRaises(Exception) as ctx:
124+
get_arm_domain("UnknownCloud")
125+
self.assertIn("Unknown cloud environment", str(ctx.exception))
126+
127+
@patch('metrics_ext_utils.metrics_ext_handler.get_arca_endpoints_from_himds')
128+
def test_arca_cloud(self, mock_arca):
129+
mock_arca.return_value = ("https://arm.azurestackcloud.example.com", "https://mcs.example.com")
130+
domain = get_arm_domain("AzureStackCloud")
131+
self.assertEqual(domain, "arm.azurestackcloud.example.com")
132+
133+
134+
class TestGetMetricsExtensionServicePath(unittest.TestCase):
135+
"""Tests for get_metrics_extension_service_path."""
136+
137+
@patch('os.path.exists')
138+
def test_lad_lib_systemd(self, mock_exists):
139+
from metrics_ext_utils.metrics_ext_handler import get_metrics_extension_service_path
140+
mock_exists.side_effect = lambda p: p == "/lib/systemd/system/"
141+
path = get_metrics_extension_service_path(True)
142+
self.assertEqual(path, metrics_constants.lad_metrics_extension_service_path)
143+
144+
@patch('os.path.exists')
145+
def test_lad_usr_lib_systemd(self, mock_exists):
146+
from metrics_ext_utils.metrics_ext_handler import get_metrics_extension_service_path
147+
mock_exists.side_effect = lambda p: p == "/usr/lib/systemd/system/"
148+
path = get_metrics_extension_service_path(True)
149+
self.assertEqual(path, metrics_constants.lad_metrics_extension_service_path_usr_lib)
150+
151+
@patch('os.path.exists')
152+
def test_lad_no_systemd_raises(self, mock_exists):
153+
from metrics_ext_utils.metrics_ext_handler import get_metrics_extension_service_path
154+
mock_exists.return_value = False
155+
with self.assertRaises(Exception):
156+
get_metrics_extension_service_path(True)
157+
158+
@patch('os.path.exists')
159+
def test_ama_etc_systemd(self, mock_exists):
160+
from metrics_ext_utils.metrics_ext_handler import get_metrics_extension_service_path
161+
mock_exists.side_effect = lambda p: p == "/etc/systemd/system"
162+
path = get_metrics_extension_service_path(False)
163+
self.assertEqual(path, metrics_constants.metrics_extension_service_path_etc)
164+
165+
@patch('os.path.exists')
166+
def test_ama_no_systemd_raises(self, mock_exists):
167+
from metrics_ext_utils.metrics_ext_handler import get_metrics_extension_service_path
168+
mock_exists.return_value = False
169+
with self.assertRaises(Exception):
170+
get_metrics_extension_service_path(False)
171+
172+
173+
class TestIsRunning(unittest.TestCase):
174+
"""Tests for is_running (metrics)."""
175+
176+
@patch('subprocess.Popen')
177+
def test_returns_true_when_process_found(self, mock_popen):
178+
from metrics_ext_utils.metrics_ext_handler import is_running
179+
mock_proc = MagicMock()
180+
mock_proc.communicate.return_value = (
181+
b"/opt/microsoft/azuremonitoragent/bin/MetricsExtension --args", b""
182+
)
183+
mock_popen.return_value = mock_proc
184+
self.assertTrue(is_running(False))
185+
186+
@patch('subprocess.Popen')
187+
def test_returns_false_when_process_not_found(self, mock_popen):
188+
from metrics_ext_utils.metrics_ext_handler import is_running
189+
mock_proc = MagicMock()
190+
mock_proc.communicate.return_value = (b"", b"")
191+
mock_popen.return_value = mock_proc
192+
self.assertFalse(is_running(False))
193+
194+
195+
class TestGetHandlerVars(unittest.TestCase):
196+
"""Tests for get_handler_vars in metrics_ext_handler."""
197+
198+
@patch('os.path.exists')
199+
def test_returns_empty_when_no_handler_env(self, mock_exists):
200+
from metrics_ext_utils.metrics_ext_handler import get_handler_vars
201+
mock_exists.return_value = False
202+
log_folder, config_folder = get_handler_vars()
203+
self.assertEqual(log_folder, "")
204+
self.assertEqual(config_folder, "")
205+
206+
@patch('os.path.exists', return_value=True)
207+
@patch('builtins.open', mock_open(read_data=json.dumps([{
208+
"handlerEnvironment": {
209+
"logFolder": "/var/log/azure/ext",
210+
"configFolder": "/etc/azure/ext"
211+
}
212+
}])))
213+
def test_parses_handler_env_list(self, mock_exists):
214+
from metrics_ext_utils.metrics_ext_handler import get_handler_vars
215+
log_folder, config_folder = get_handler_vars()
216+
self.assertEqual(log_folder, "/var/log/azure/ext")
217+
self.assertEqual(config_folder, "/etc/azure/ext")
218+
219+
220+
if __name__ == '__main__':
221+
unittest.main()

0 commit comments

Comments
 (0)