Skip to content

Commit 885a073

Browse files
authored
Merge pull request #19 from srl-labs/grafana_flow
Grafana, switch to flow panel
2 parents db874b5 + bb5c05d commit 885a073

8 files changed

+240
-579
lines changed

clab2drawio.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -1218,12 +1218,23 @@ def main(
12181218
output_filename = os.path.basename(grafana_output_file)
12191219
diagram.grafana_dashboard_file = grafana_output_file
12201220
os.makedirs(output_folder, exist_ok=True)
1221+
12211222
grafana = GrafanaDashboard(diagram)
1222-
grafana_json = grafana.create_dashboard()
1223-
# dump the json to the file
1223+
1224+
# Create flow panel YAML
1225+
panel_config = grafana.create_panel_yaml()
1226+
1227+
# Write flow panel YAML to file
1228+
flow_panel_output_file = os.path.splitext(grafana_output_file)[0] + ".flow_panel.yaml"
1229+
with open(flow_panel_output_file, "w") as f:
1230+
f.write(panel_config)
1231+
print("Saved flow panel YAML to:", flow_panel_output_file)
1232+
1233+
grafana_json = grafana.create_dashboard(panel_config)
1234+
# Dump the JSON to the file
12241235
with open(grafana_output_file, "w") as f:
12251236
f.write(grafana_json)
1226-
print("Saved file to:", grafana_output_file)
1237+
print("Saved Grafana dashboard JSON to:", grafana_output_file)
12271238
else:
12281239
add_links(diagram, styles)
12291240

docs/grafana.md

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### Grafana Dashboard Generation Option
22

3-
The `-g, --gf_dashboard` command line option is designed to automate the generation of Grafana dashboards with all it's rules from your YAML configuration files. This feature is currently in a **work-in-progress (WIP)** stage. When using this option, it is important to note the following specifics:
3+
The `-g, --gf_dashboard` command line option is designed to automate the generation of Grafana dashboards with all it's rules from your YAML configuration files. When using this option, it is important to note the following specifics:
44

55
![Grafana ](img/grafana.png)
66

@@ -10,16 +10,32 @@ The `-g, --gf_dashboard` command line option is designed to automate the generat
1010
- All trafic is outgoing metric
1111

1212
#### Compatibility
13-
- **Grafana Version:** This option is tailored to work optimally with Grafana version **10.3.5**. Buggy from 10.4 upwords
14-
- **Plugin Requirement:** It requires the Flowcharting plugin version **1.0.0.e**, which is available via a specific fork maintained by [skyfrank on GitHub](https://github.com/skyfrank/grafana-flowcharting). This plugin is essential for rendering the custom visualizations generated by the script. Lower version also work, but this one is recommended
13+
- **Grafana Version:** Works optimally with Grafana version **>10.0.0**. Recomendation: **11.2.0**.
14+
- **Plugin Requirement:** It requires the Flowcplugin [Flow plugin](https://grafana.com/grafana/plugins/andrewbmchugh-flow-panel). This plugin is essential for rendering the custom visualizations generated by the script.
1515

16-
#### Usage
16+
### Usage
1717
To generate a dashboard, execute the following command:
1818
```bash
1919
python clab2drawio.py -i <path_to_your_yaml_file> -g --theme grafana_dark
2020
```
2121
Ensure that you replace `<path_to_your_yaml_file>` with the actual path to your YAML configuration file. Use it with grafana_dark or your own grafana compatible theme.
2222

23+
When the `-g` flag is used, the script generates the following:
24+
1. Grafana dashboard JSON file
25+
2. Panel YAML configuration file
26+
3. draw.io diagram
27+
28+
#### To export the diagram as an SVG:
29+
To get a full guide: [https://github.com/andymchugh/andrewbmchugh-flow-panel/blob/main/src/README.md#using-drawio-to-create-your-svg](https://github.com/andymchugh/andrewbmchugh-flow-panel/blob/main/src/README.md#using-drawio-to-create-your-svg)
30+
1. Open the generated draw.io diagram using the draw.io application with the svgdata plugin enabled, or use the online version at [https://app.diagrams.net/?p=svgdata](https://app.diagrams.net/?p=svgdata).
31+
2. Go to File -> Export -> SVG to export the diagram as an SVG file.
32+
33+
The generated dashboard JSON will include the panel configuration but without the SVG data. To complete the dashboard, you need to either:
34+
- Copy and paste the SVG data into the designated SVG box in the Grafana dashboard editor.
35+
- Upload the SVG file to a hosting service and reference the URL in the Grafana dashboard editor.
36+
37+
By following these steps, you can generate a complete Grafana dashboard with the diagram, panel configuration, and dashboard JSON file.
38+
2339
#### Current Limitations
2440
- **Hardcoded Queries:** Currently, the dashboard queries are hardcoded and are specifically optimized for Nokia's SRLinux and SROS platforms. This means they may not be directly applicable to other environments without modifications.
2541
- **Data Sources:** The dashboard assumes specific data sources (Prometheus) are already configured in your Grafana instance that align with the hardcoded queries.

lib/Grafana.py

+100-177
Original file line numberDiff line numberDiff line change
@@ -1,197 +1,120 @@
11
import json
22
import os
33
import xml.etree.ElementTree as ET
4-
4+
from ruamel.yaml import YAML
5+
from ruamel.yaml.comments import CommentedMap, CommentedSeq
6+
import yaml
57

68
class GrafanaDashboard:
7-
def __init__(self, diagram=None):
9+
def __init__(self, diagram=None, panel_config=None):
810
self.diagram = diagram
911
self.links = self.diagram.get_links_from_nodes()
1012
self.dashboard_filename = self.diagram.grafana_dashboard_file
1113

12-
def create_dashboard(self):
13-
# We just need the subtree objects from mxGraphModel.Single page drawings only
14-
xmlTree = ET.fromstring(self.diagram.dump_xml())
15-
subXmlTree = xmlTree.findall(".//mxGraphModel")[0]
16-
17-
# Define Query rules for the Panel, rule_expr needs to match the collector metric name
18-
# Legend format needs to match the format expected by the metric
19-
panelQueryList = {
20-
"IngressTraffic": {
21-
"rule_expr": "interface_traffic_rate_in_bps",
22-
"legend_format": "{{source}}:{{interface_name}}:in",
23-
},
24-
"EgressTraffic": {
25-
"rule_expr": "interface_traffic_rate_out_bps",
26-
"legend_format": "{{source}}:{{interface_name}}:out",
27-
},
28-
"ItfOperState": {
29-
"rule_expr": "interface_oper_state",
30-
"legend_format": "oper_state:{{source}}:{{interface_name}}",
31-
},
32-
"ItfOperState2": {
33-
"rule_expr": "port_oper_state",
34-
"legend_format": "oper_state:{{source}}:{{interface_name}}",
35-
},
36-
"EgressTraffic2": {
37-
"rule_expr": "irate(port_ethernet_statistics_out_octets[$__rate_interval])*8",
38-
"legend_format": "{{source}}:{{interface_name}}:out",
39-
},
40-
}
41-
# Create a targets list to embed in the JSON object, we add all the other default JSON attributes to the list
42-
targetsList = []
43-
for query in panelQueryList:
44-
targetsList.append(
45-
self.gf_dashboard_datasource_target(
46-
rule_expr=panelQueryList[query]["rule_expr"],
47-
legend_format=panelQueryList[query]["legend_format"],
48-
refId=query,
49-
)
50-
)
51-
52-
# Create the Rules Data
53-
rulesData = []
54-
i = 0
55-
for link in self.links:
56-
link_id = f"link_id:{link.source.name}:{link.source_intf}:{link.target.name}:{link.target_intf}"
57-
58-
# Traffic out
59-
rulesData.append(
60-
self.gf_flowchart_rule_traffic(
61-
ruleName=f"{link.source.name}:{link.source_intf}:out",
62-
metric=f"{link.source.name.lower()}:{link.source_intf}:out",
63-
link_id=link_id,
64-
order=i,
65-
)
66-
)
67-
68-
i = i + 2
69-
70-
port_id = f"{link.source.name}:{link.source_intf}:{link.target.name}:{link.target_intf}"
71-
# Port State:
72-
rulesData.append(
73-
self.gf_flowchart_rule_operstate(
74-
ruleName=f"oper_state:{link.source.name}:{link.source_intf}",
75-
metric=f"oper_state:{link.source.name.lower()}:{link.source_intf}",
76-
link_id=port_id,
77-
order=i + 3,
78-
)
79-
)
80-
i = i + 2
81-
82-
# Create the Panel
83-
flowchart_panel = self.gf_flowchart_panel_template(
84-
xml=ET.tostring(subXmlTree, encoding="unicode"),
85-
rulesData=rulesData,
86-
panelTitle="Network Telemetry",
87-
targetsList=targetsList,
88-
)
89-
# Create a dashboard from the panel
90-
dashboard_json = json.dumps(
91-
self.gf_dashboard_template(
92-
panels=flowchart_panel,
93-
dashboard_name=os.path.splitext(self.dashboard_filename)[0],
94-
),
95-
indent=4,
96-
)
97-
return dashboard_json
98-
99-
def gf_dashboard_datasource_target(
100-
self, rule_expr="promql_query", legend_format=None, refId="Query1"
101-
):
102-
"""
103-
Dictionary containing information relevant to the Targets queried
104-
"""
105-
target = {
106-
"datasource": {"type": "prometheus", "uid": "${DS_PROMETHEUS}"},
107-
"editorMode": "code",
108-
"expr": rule_expr,
109-
"instant": False,
110-
"legendFormat": legend_format,
111-
"range": True,
112-
"refId": refId,
113-
}
114-
return target
115-
116-
def gf_flowchart_rule_traffic(
117-
self, ruleName="traffic:inOrOut", metric=None, link_id=None, order=1
118-
):
119-
"""
120-
Dictionary containing information relevant to the traffic Rules
121-
"""
122-
# Load the traffic rule template from file
14+
def create_dashboard(self, panel_config):
15+
# Path to the dashboard JSON template
12316
base_dir = os.getenv("APP_BASE_DIR", "")
17+
template_path = os.path.join(base_dir, "lib/templates/flow_panel_template.json")
12418

125-
with open(
126-
os.path.join(base_dir, "lib/templates/traffic_rule_template.json"), "r"
127-
) as f:
128-
rule = json.load(f)
129-
130-
rule["alias"] = ruleName
131-
rule["pattern"] = metric
132-
rule["mapsDat"]["shapes"]["dataList"][0]["pattern"] = link_id
133-
rule["mapsDat"]["texts"]["dataList"][0]["pattern"] = link_id
134-
rule["order"] = order
135-
136-
return rule
137-
138-
def gf_flowchart_rule_operstate(
139-
self, ruleName="oper_state", metric=None, link_id=None, order=1
140-
):
141-
"""
142-
Dictionary containing information relevant to the Operational State Rules
143-
"""
144-
# Load the operstate rule template from file
145-
base_dir = os.getenv("APP_BASE_DIR", "")
19+
# Load the dashboard template from file
20+
with open(template_path, 'r') as file:
21+
dashboard_json = json.load(file)
14622

147-
with open(
148-
os.path.join(base_dir, "lib/templates/operstate_rule_template.json"), "r"
149-
) as f:
150-
rule = json.load(f)
151-
152-
rule["alias"] = ruleName
153-
rule["pattern"] = metric
154-
rule["mapsDat"]["shapes"]["dataList"][0]["pattern"] = link_id
155-
rule["order"] = order
156-
157-
return rule
158-
159-
def gf_flowchart_panel_template(
160-
self, xml=None, rulesData=None, targetsList=None, panelTitle="Network Topology"
161-
):
162-
"""
163-
Dictionary containing information relevant to the Panels Section in the JSON Dashboard
164-
Embedding of the XML diagram, the Rules and the Targets
165-
"""
166-
# Load the panel template from file
167-
base_dir = os.getenv("APP_BASE_DIR", "")
16823

169-
with open(
170-
os.path.join(base_dir, "lib/templates/panel_template.json"), "r"
171-
) as f:
172-
panel = json.load(f)
24+
# Insert the YAML configuration as a string into the panelConfig of the relevant panel
25+
for panel in dashboard_json['panels']:
26+
if 'options' in panel:
27+
panel['options']['panelConfig'] = panel_config
17328

174-
panel[0]["flowchartsData"]["flowcharts"][0]["xml"] = xml
175-
panel[0]["rulesData"]["rulesData"] = rulesData
176-
panel[0]["targets"] = targetsList
177-
panel[0]["title"] = panelTitle
29+
return json.dumps(dashboard_json, indent=2)
17830

179-
return panel
31+
def create_panel_yaml(self):
32+
from ruamel.yaml import YAML, CommentedMap, CommentedSeq
18033

181-
def gf_dashboard_template(self, panels=None, dashboard_name="lab-telemetry"):
182-
"""
183-
Dictionary containing information relevant to the Grafana Dashboard Root JSON object
184-
"""
34+
yaml = YAML()
35+
yaml.explicit_start = True # To include '---' at the start
36+
yaml.width = 4096 # prevent line wrapping
18537

186-
base_dir = os.getenv("APP_BASE_DIR", "")
38+
root = CommentedMap()
18739

188-
# Load the dashboard template from file
189-
with open(
190-
os.path.join(base_dir, "lib/templates/traffic_rule_template.json")
191-
) as f:
192-
dashboard = json.load(f)
40+
# Anchors and Aliases
41+
thresholds_operstate = CommentedSeq()
42+
thresholds_operstate.append({'color': 'red', 'level': 0})
43+
thresholds_operstate.append({'color': 'green', 'level': 1})
44+
45+
thresholds_operstate.yaml_set_anchor('thresholds-operstate', always_dump=True)
46+
47+
thresholds_traffic = CommentedSeq()
48+
thresholds_traffic.append({'color': 'gray', 'level': 0})
49+
thresholds_traffic.append({'color': 'green', 'level': 199999})
50+
thresholds_traffic.append({'color': 'yellow', 'level': 500000})
51+
thresholds_traffic.append({'color': 'orange', 'level': 1000000})
52+
thresholds_traffic.append({'color': 'red', 'level': 5000000})
53+
54+
thresholds_traffic.yaml_set_anchor('thresholds-traffic', always_dump=True)
55+
56+
label_config = CommentedMap()
57+
label_config['separator'] = "replace"
58+
label_config['units'] = "bps"
59+
label_config['decimalPoints'] = 1
60+
label_config['valueMappings'] = [
61+
{'valueMax': 199999, 'text': "\u200B"},
62+
{'valueMin': 200000}
63+
]
64+
65+
label_config.yaml_set_anchor('label-config', always_dump=True)
66+
67+
# Anchors entry in root
68+
root['anchors'] = anchors = CommentedMap()
69+
70+
anchors['thresholds-operstate'] = thresholds_operstate
71+
anchors['thresholds-traffic'] = thresholds_traffic
72+
anchors['label-config'] = label_config
73+
74+
# cellIdPreamble
75+
root['cellIdPreamble'] = 'cell-'
76+
77+
# cells
78+
cells = CommentedMap()
79+
root['cells'] = cells
80+
for link in self.links:
81+
source_name = link.source.name
82+
source_intf = link.source_intf
83+
target_name = link.target.name
84+
target_intf = link.target_intf
85+
86+
# Operstate cell
87+
cell_id_operstate = f"{source_name}:{source_intf}:{target_name}:{target_intf}"
88+
dataRef_operstate = f"oper-state:{source_name}:{source_intf}"
89+
90+
# fillColor thresholds referencing the anchor
91+
fillColor_operstate = CommentedMap()
92+
fillColor_operstate['thresholds'] = thresholds_operstate # reference anchor
93+
94+
cell_operstate = CommentedMap()
95+
cell_operstate['dataRef'] = dataRef_operstate
96+
cell_operstate['fillColor'] = fillColor_operstate
97+
98+
cells[cell_id_operstate] = cell_operstate
99+
100+
# Traffic cell
101+
cell_id_traffic = f"link_id:{source_name}:{source_intf}:{target_name}:{target_intf}"
102+
103+
dataRef_traffic = f"{source_name}:{source_intf}:out"
104+
105+
strokeColor_traffic = CommentedMap()
106+
strokeColor_traffic['thresholds'] = thresholds_traffic # reference anchor
107+
108+
cell_traffic = CommentedMap()
109+
cell_traffic['dataRef'] = dataRef_traffic
110+
cell_traffic['label'] = label_config # reference anchor
111+
cell_traffic['strokeColor'] = strokeColor_traffic
193112

194-
dashboard["panels"] = panels
195-
dashboard["title"] = dashboard_name
113+
cells[cell_id_traffic] = cell_traffic
196114

197-
return dashboard
115+
# Now write root to YAML
116+
import io
117+
stream = io.StringIO()
118+
yaml.dump(root, stream)
119+
panel_yaml = stream.getvalue()
120+
return panel_yaml

0 commit comments

Comments
 (0)