Skip to content

Commit 5b7e772

Browse files
authored
Merge pull request #191 from stfc/weekly-reporting
Add weekly reporting script
2 parents 1b08f5e + b825508 commit 5b7e772

File tree

4 files changed

+299
-0
lines changed

4 files changed

+299
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,6 @@ One terraform script to create a private network on openstack along with a route
9999
A Python script that when run, creates a filter word cloud from the summary of tickets over a time period.
100100
[More Here](word_cloud_generator/)
101101

102+
# Weekly-Reporting
103+
A Python script to upload data to the weekly reporting InfluxDB instance for the Cloud statistics reporting.
104+
[More Here](Weekly-Reporting/)

Weekly-Reporting/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Weekly Reporting
2+
3+
This script is used to upload a weekly report to an InfluxDB instance. It tags all the data points with the time when ran.<br>
4+
If you need to back-fill data or use a different time you can do the below in the code:<br>
5+
```
6+
...
7+
16 # time = datetime.datetime.now().isoformat()
8+
17 time = "2025-01-02T15:17:37.780483"
9+
...
10+
```
11+
The script uses argparse to provide an easy-to-use command line interface.<br>
12+
There is a template yaml file [here](data.yaml) which **requires all values to be not empty**.
13+
14+
> **NOTE on data.yaml:**
15+
> - Values in data.yaml must not be empty.
16+
> - Values which can be floating point such as Memory in TB must have .0 for whole numbers.
17+
18+
## Instructions:
19+
20+
1. Fill in `data.yaml` with your reporting data.
21+
2. Write your token in plain text in a file e.g. "token"
22+
2. Run `export.py` with the correct arguments, see below:
23+
24+
```shell
25+
python3 export.py --host="http://172.16.103.52:8086" \
26+
--org="cloud" \
27+
--bucket="weekly-reports-time"
28+
--token-file="token"
29+
--report-file="data.yaml"
30+
```

Weekly-Reporting/data.yaml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# This is the data file for the weekly statistics report exporter.
2+
#
3+
# Note the below.
4+
#
5+
# Every field must have a value.
6+
# Where values are floating type, they must remain floating type.
7+
# For example, if memory_in_use changes from 234.11 TB to 212 TB.
8+
# You must enter 212 TB as 212.0 TB
9+
10+
11+
cpu:
12+
# Cores in the Cloud
13+
in_use:
14+
total:
15+
16+
memory:
17+
# Memory in the Cloud (TB)
18+
in_use:
19+
total:
20+
21+
storage:
22+
# Storage stats by type (TB)
23+
sirius:
24+
in_use:
25+
total:
26+
nodes:
27+
deneb:
28+
in_use:
29+
total:
30+
arided:
31+
in_use:
32+
total:
33+
34+
hv:
35+
# HV stats
36+
active:
37+
active:
38+
cpu_full:
39+
memory_full:
40+
down:
41+
disabled:
42+
43+
vm:
44+
# VM stats
45+
active:
46+
shutoff:
47+
errored:
48+
building:
49+
50+
floating_ip:
51+
# Floating IP
52+
in_use:
53+
total:
54+
55+
virtual_worker_nodes:
56+
# Condor nodes
57+
active:

Weekly-Reporting/export.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"""Export weekly reports statistics data to an InfluxDB bucket."""
2+
from pprint import pprint
3+
from typing import Dict, List
4+
5+
from influxdb_client import Point
6+
from pathlib import Path
7+
import argparse
8+
import yaml
9+
import configparser
10+
import logging
11+
import datetime
12+
13+
from influxdb_client.client import influxdb_client
14+
from influxdb_client.client.write_api import SYNCHRONOUS
15+
16+
time = datetime.datetime.now().isoformat()
17+
# time = "2025-01-02T15:17:37.780483"
18+
19+
20+
def main(args: argparse.Namespace):
21+
"""
22+
Main entry point to script.
23+
:param args: Arguments provided on the command line.
24+
"""
25+
_check_args(args)
26+
points = []
27+
if args.report_file:
28+
points = _create_points_report(args.report_file)
29+
elif args.inventory_file:
30+
points = _create_points_inventory(args.inventory_file)
31+
api_token = _get_token(args.token_file)
32+
_write_data(
33+
points=points,
34+
host=args.host,
35+
org=args.org,
36+
bucket=args.bucket,
37+
token=api_token
38+
)
39+
40+
41+
def _check_args(args: argparse.Namespace):
42+
"""
43+
Check the correct arguments are provided
44+
:param args: Argparse namespace
45+
"""
46+
if not args.host:
47+
raise RuntimeError("Argument --host not given.")
48+
if not args.org:
49+
raise RuntimeError("Argument --org not given.")
50+
if not args.bucket:
51+
raise RuntimeError("Argument --bucket not given.")
52+
if not args.token_file:
53+
raise RuntimeError("Argument --token-file not given.")
54+
if not args.report_file and not args.inventory_file:
55+
raise RuntimeError("Argument --report-file or --inventory-file not given.")
56+
if args.report_file and args.inventory_file:
57+
raise RuntimeError("Argument --report-file and --inventory-file given. Only one data file can be provided.")
58+
if not Path(args.token_file).is_file():
59+
raise RuntimeError(f"Cannot find token file at path {args.token_file}.")
60+
if args.report_file and not Path(args.report_file).is_file():
61+
raise RuntimeError(f"Cannot find report file at path {args.report_file}.")
62+
if args.inventory_file and not Path(args.inventory_file).is_file():
63+
raise RuntimeError(f"Cannot find inventory file at path {args.inventory_file}.")
64+
65+
66+
def _get_token(file_path: str) -> str:
67+
"""
68+
Get the token from the token file.
69+
:param file_path: File path to token file
70+
:return: Token as string
71+
"""
72+
with open(Path(file_path), "r", encoding="utf-8") as file:
73+
token = file.read().strip()
74+
return token
75+
76+
77+
def _create_points_report(file_path: str) -> List[Point]:
78+
"""
79+
Create a list of Influx points from the data.
80+
:param file_path: Path to data file
81+
:return: List of Points.
82+
"""
83+
points = []
84+
with open(Path(file_path), "r", encoding="utf-8") as file:
85+
data = yaml.safe_load(file)
86+
87+
for key, value in data.items():
88+
points += _from_key(key, value)
89+
90+
return points
91+
92+
93+
def _create_points_inventory(file_path: str) -> List[Point]:
94+
"""
95+
Create a list of Influx points from the data.
96+
:param file_path: Path to data file
97+
:return: List of Points.
98+
"""
99+
points = []
100+
config = configparser.ConfigParser()
101+
config.read(file_path)
102+
pprint(config)
103+
104+
return points
105+
106+
107+
def _from_key(key: str, data: Dict) -> List[Point]:
108+
"""
109+
Decide which create_point method to call from the key.
110+
:param key: Section of data. For example, CPU
111+
:param data: The values of the section
112+
:return: List of points
113+
"""
114+
if key == "cpu":
115+
return _from_cpu(data)
116+
if key == "memory":
117+
return _from_memory(data)
118+
if key == "storage":
119+
return _from_storage(data)
120+
if key == "hv":
121+
return _from_hv(data)
122+
if key == "vm":
123+
return _from_vm(data)
124+
if key == "floating_ip":
125+
return _from_fip(data)
126+
if key == "virtual_worker_nodes":
127+
return _from_vwn(data)
128+
else:
129+
raise RuntimeError(f"Key {key} not supported. Please contact service maintainer.")
130+
131+
132+
def _from_cpu(data: Dict) -> List[Point]:
133+
"""Extract cpu data from yaml into a Point."""
134+
return [Point("cpu").field("in_use", data["in_use"]).field("total", data["total"]).time(time)]
135+
136+
137+
def _from_memory(data: Dict) -> List[Point]:
138+
"""Extract memory data from yaml into a Point."""
139+
140+
return [Point("memory").field("in_use", data["in_use"]).field("total", data["total"]).time(time)]
141+
142+
143+
def _from_storage(data: Dict) -> List[Point]:
144+
"""Extract storage data from yaml into Points."""
145+
points = []
146+
for key, value in data.items():
147+
for key_2, value_2 in value.items():
148+
points.append(Point(key).field(key_2, value_2).time(time))
149+
return points
150+
151+
152+
def _from_hv(data: Dict) -> List[Point]:
153+
"""Extract hv data from yaml into Points."""
154+
points = []
155+
points.append(Point("hv").field("active", data["active"]["active"]).time(time))
156+
points.append(Point("hv").field("active_and_cpu_full", data["active"]["cpu_full"]).time(time))
157+
points.append(Point("hv").field("active_and_memory_full", data["active"]["memory_full"]).time(time))
158+
points.append(Point("hv").field("down", data["down"]).time(time))
159+
points.append(Point("hv").field("disabled", data["disabled"]).time(time))
160+
return points
161+
162+
163+
def _from_vm(data: Dict) -> List[Point]:
164+
"""Extract vm data from yaml into a Point."""
165+
return [(
166+
Point("vm")
167+
.field("active", data["active"])
168+
.field("shutoff", data["shutoff"])
169+
.field("errored", data["errored"])
170+
.field("building", data["building"])
171+
.time(time))]
172+
173+
174+
def _from_fip(data: Dict) -> List[Point]:
175+
"""Extract floating ip data from yaml into a Point."""
176+
return [Point("floating_ip").field("in_use", data["in_use"]).field("total", data["total"]).time(time)]
177+
178+
179+
def _from_vwn(data: Dict) -> List[Point]:
180+
"""Extract virtual worker nodes data from yaml into a Point."""
181+
return [Point("virtual_worker_nodes").field("active", data["active"]).time(time)]
182+
183+
184+
def _write_data(points: List[Point], host: str, org: str, bucket: str, token: str):
185+
"""
186+
Write the data to the InfluxDB bucket.
187+
:param points: Points to write
188+
:param host: Host URL
189+
:param org: InfluxDB organisation
190+
:param bucket: InfluxDB bucket
191+
:param token: InfluxDB API access token
192+
"""
193+
with influxdb_client.InfluxDBClient(url=host, token=token, org=org) as _client:
194+
with _client.write_api(write_options=SYNCHRONOUS) as _write_api:
195+
_write_api.write(bucket, org, points)
196+
197+
198+
if __name__ == "__main__":
199+
parser = argparse.ArgumentParser()
200+
parser.add_argument("--host", help="InfluxDB host url with port.")
201+
parser.add_argument("--org", help="InfluxDB organisation.")
202+
parser.add_argument("--bucket", help="InfluxDB bucket to write to.")
203+
parser.add_argument("--token-file", help="InfluxDB access token file path.")
204+
parser.add_argument("--report-file", help="Report yaml file.")
205+
parser.add_argument("--inventory-file", help="Inventory ini file.")
206+
arguments = parser.parse_args()
207+
main(arguments)
208+
209+

0 commit comments

Comments
 (0)