Skip to content

Commit 51115c3

Browse files
authored
Merge pull request #383 from stfc/aggregate_diff
Aggregate metadata diff sensor
2 parents f7233d3 + f8a5d50 commit 51115c3

11 files changed

+524
-138
lines changed

.github/workflows/unit_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
matrix:
1414
# 3.9+ is not currently supported due to https://github.com/freach/udatetime/issues/32
15-
python-version: ["3.8", "3.9"]
15+
python-version: ["3.8", "3.9", "3.10"]
1616
steps:
1717
- uses: actions/checkout@v5
1818
- name: Set up Python ${{ matrix.python-version }}

lib/apis/utils/__init__.py

Whitespace-only changes.

lib/apis/utils/diff_table.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import deepdiff
2+
from deepdiff import extract
3+
4+
5+
def diff_to_tabulate_table(obj1, obj2, excluded_paths=None):
6+
"""
7+
Formats the output from DeepDiff into a list of lists suitable for tabulate,
8+
showing path, before, and after values.
9+
"""
10+
diff_output = deepdiff.DeepDiff(
11+
t1=obj1,
12+
t2=obj2,
13+
exclude_paths=excluded_paths,
14+
threshold_to_diff_deeper=0,
15+
ignore_order=True,
16+
)
17+
18+
table_data = []
19+
20+
# Handle 'values_changed'
21+
if "values_changed" in diff_output:
22+
for path, change_info in diff_output["values_changed"].items():
23+
table_data.append(
24+
[
25+
path,
26+
change_info.get("old_value", "Not Present"),
27+
change_info.get("new_value", "Not Present"),
28+
]
29+
)
30+
31+
# Handle 'dictionary_item_added'
32+
if "dictionary_item_added" in diff_output:
33+
for path in diff_output["dictionary_item_added"]:
34+
table_data.append(
35+
[
36+
path,
37+
"Not Present", # No old value for added items
38+
extract(obj2, path),
39+
]
40+
)
41+
42+
# Handle 'dictionary_item_removed'
43+
if "dictionary_item_removed" in diff_output:
44+
for path in diff_output["dictionary_item_removed"]:
45+
table_data.append(
46+
[
47+
path,
48+
extract(obj1, path),
49+
"Not Present", # No new value for removed items
50+
]
51+
)
52+
53+
# Handle 'iterable_item_added'
54+
if "iterable_item_added" in diff_output:
55+
for path, value in diff_output["iterable_item_added"].items():
56+
table_data.append([path, "N/A", value])
57+
58+
# Handle 'iterable_item_removed'
59+
if "iterable_item_removed" in diff_output:
60+
for path, value in diff_output["iterable_item_removed"].items():
61+
table_data.append([path, value, "N/A"])
62+
63+
# Handle 'type_changes'
64+
if "type_changes" in diff_output:
65+
for path, change_info in diff_output["type_changes"].items():
66+
table_data.append(
67+
[
68+
path,
69+
f"'{change_info.get('old_value')}' (Type: {change_info.get('old_type').__name__})",
70+
f"'{change_info.get('new_value')}' (Type: {change_info.get('new_type').__name__})",
71+
]
72+
)
73+
74+
return table_data
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
name: "alert.aggregate.metadata.mismatch"
3+
pack: "stackstorm_openstack"
4+
description: "Sends Jira Ticket for Aggregate metadata mismatch between prod and dev clouds"
5+
enabled: true
6+
7+
trigger:
8+
type: "stackstorm_openstack.aggregate.metadata_mismatch"
9+
10+
action:
11+
ref: "jira.create_issue"
12+
parameters:
13+
summary: "[Stackstorm Alert]: {{trigger.aggregate_name}} Aggregate metadata mismatch between prod and dev clouds"
14+
type: Problem
15+
description: >
16+
{{trigger.diff}}

sensors/aggregate.aggregate_list.yaml

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
class_name: HostAggregateSensor
3+
entry_point: src/host_aggregate_metadata_sensor.py
4+
description: Detect changes in host aggregates
5+
poll_interval: 604800 # 7 days
6+
enabled: false
7+
trigger_types:
8+
- name: "aggregate.metadata_mismatch"
9+
description: "Triggers for aggregate metadata mismatch"
10+
payload_schema:
11+
type: "object"
12+
properties:
13+
diff:
14+
type: "string"
15+
description: "diff between aggregate"

sensors/src/host_aggregate_list_sensor.py

Lines changed: 0 additions & 73 deletions
This file was deleted.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import tabulate
2+
3+
from apis.openstack_api.openstack_connection import OpenstackConnection
4+
from apis.utils.diff_table import diff_to_tabulate_table
5+
from st2reactor.sensor.base import PollingSensor
6+
7+
8+
class HostAggregateSensor(PollingSensor):
9+
"""
10+
* self.sensor_service
11+
- provides utilities like
12+
get_logger() for writing to logs.
13+
dispatch() for dispatching triggers into the system.
14+
* self._config
15+
- contains configuration that was specified as
16+
config.yaml in the pack.
17+
* self._poll_interval
18+
- indicates the interval between two successive poll() calls.
19+
"""
20+
21+
def __init__(self, sensor_service, config=None, poll_interval=None):
22+
super().__init__(
23+
sensor_service=sensor_service, config=config, poll_interval=poll_interval
24+
)
25+
self._log = self._sensor_service.get_logger(__name__)
26+
self.source_cloud = self.config["sensor_source_cloud"]
27+
self.target_cloud = self.config["sensor_dest_cloud"]
28+
29+
def setup(self):
30+
"""
31+
Stub
32+
"""
33+
34+
def poll(self):
35+
"""
36+
Polls the dev cloud host aggregates and dispatches a payload containing
37+
a list of aggregates.
38+
"""
39+
with OpenstackConnection(self.source_cloud) as source_conn, OpenstackConnection(
40+
self.target_cloud
41+
) as target_conn:
42+
43+
source_aggregates = {
44+
agg.name: agg for agg in source_conn.compute.aggregates()
45+
}
46+
target_aggregates = {
47+
agg.name: agg for agg in target_conn.compute.aggregates()
48+
}
49+
50+
self._log.info(
51+
"Compare source (%s) and target (%s) host aggregate metadata"
52+
)
53+
for aggregate_name, source_agg in source_aggregates.items():
54+
target_agg = target_aggregates.get(aggregate_name)
55+
56+
if not target_agg:
57+
self._log.info(
58+
"aggregate %s doesn't exist in %s cloud",
59+
aggregate_name,
60+
self.target_cloud,
61+
)
62+
continue
63+
64+
diff = diff_to_tabulate_table(
65+
obj1=source_agg,
66+
obj2=target_agg,
67+
excluded_paths=[
68+
"root['hosts']",
69+
"root['created_at']",
70+
"root['updated_at']",
71+
"root['uuid']",
72+
"root['id']",
73+
"root['location']",
74+
],
75+
)
76+
77+
if diff:
78+
79+
self._log.info(
80+
"aggregate metadata mismatch between source (%s) and target (%s): %s",
81+
self.source_cloud,
82+
self.target_cloud,
83+
aggregate_name,
84+
)
85+
86+
headers = [
87+
"Path",
88+
self.source_cloud,
89+
self.target_cloud,
90+
]
91+
92+
payload = {
93+
"aggregate_name": source_agg.name,
94+
"diff": tabulate.tabulate(
95+
diff,
96+
headers=headers,
97+
tablefmt="jira",
98+
),
99+
}
100+
101+
self.sensor_service.dispatch(
102+
trigger="stackstorm_openstack.aggregate.metadata_mismatch",
103+
payload=payload,
104+
)
105+
106+
def cleanup(self):
107+
"""
108+
Stub
109+
"""
110+
111+
def add_trigger(self, trigger):
112+
"""
113+
Stub
114+
"""
115+
116+
def update_trigger(self, trigger):
117+
"""
118+
Stub
119+
"""
120+
121+
def remove_trigger(self, trigger):
122+
"""
123+
Stub
124+
"""

0 commit comments

Comments
 (0)