Skip to content

Commit 15a1bf3

Browse files
authored
Merge branch 'master' into alexy_ck_job_6
2 parents 1cf9456 + 21aff9b commit 15a1bf3

16 files changed

Lines changed: 278 additions & 138 deletions

File tree

.zuul.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@
138138
139139
- job:
140140
name: feature-verification-tests-noop
141-
parent: noop
142141
description: |
143142
A job that always passes. Runs when there's a change to jobs that don't
144143
need full zuul to run but still need to report a pass.
@@ -149,6 +148,7 @@
149148
name: functional-periodic-telemetry-with-ceph
150149
parent: podified-multinode-hci-deployment-crc-1comp-backends
151150
dependencies: ["telemetry-openstack-meta-content-provider-master"]
151+
nodeset: telemetry-operator-all-the-features
152152
description: |
153153
Deploy OpenStack with Telemetry and Ceph. Run metric-verification with volume pool metric tests
154154
extra-vars: *functional_autoscaling_extra_vars
@@ -241,4 +241,3 @@
241241
# case the job definition changes.
242242
- ^roles/telemetry_verify_metrics/tasks/verify_ceilometer_volume_pool_metrics.yml$
243243
- ^.zuul.yaml$
244-

roles/telemetry_chargeback/defaults/main.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ cloudkitty_debug: false
77
cloudkitty_debug_dir: "{{ (cloudkitty_debug | bool) | ternary(artifacts_dir_zuul + '/debug_ck_db', '') }}"
88

99
# Directory paths
10-
logs_dir_zuul: "{{ ansible_env.HOME }}/ci-framework-data/logs"
11-
artifacts_dir_zuul: "{{ ansible_env.HOME }}/ci-framework-data/artifacts"
12-
cert_dir: "{{ ansible_user_dir }}/ck-certs"
13-
local_cert_dir: "{{ ansible_env.HOME }}/ci-framework-data/flush_certs"
10+
logs_dir_zuul: "{{ cifmw_basedir }}/logs"
11+
artifacts_dir_zuul: "{{ cifmw_basedir }}/artifacts"
12+
cert_dir: "{{ cifmw_basedir }}/ck-certs"
13+
local_cert_dir: "{{ cifmw_basedir }}/flush_certs"
1414
remote_cert_dir: "osp-certs"
1515

1616
# Cloudkitty certificates and secrets

roles/telemetry_chargeback/files/gen_db_summary.py

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,11 @@ def _apply_mutate(qty: float, mutate: str) -> float:
118118
elif mutate_upper == "FLOOR":
119119
return math.floor(qty)
120120
elif mutate_upper == "NUMBOOL":
121-
# If qty equals 0, leave it at 0. Else, set it to 1.
121+
# If qty near 0, set it at 0. Else, set it to 1.
122122
return 0.0 if abs(qty) < 1e-9 else 1.0
123123
elif mutate_upper == "NOTNUMBOOL":
124-
# If qty equals 0, set it to 1. Else, set it to 0.
125-
return 1.0 if qty == 0 else 0.0
124+
# If qty near 0, set it to 1. Else, set it to 0.
125+
return 1.0 if abs(qty) < 1e-9 else 0.0
126126
else: # NONE or any unrecognized value
127127
return qty
128128

@@ -220,6 +220,11 @@ def aggregate_rates_by_type(
220220

221221

222222
def build_summary(pairs: list[tuple[str, str]]) -> dict[str, Any]:
223+
# Early exit if no pairs
224+
if not pairs:
225+
print("Error: No log entries to summarize", file=sys.stderr)
226+
sys.exit(1)
227+
223228
log_count = len(pairs)
224229
per_ts = Counter(ts for ts, _ in pairs)
225230
n_ts = len(per_ts)
@@ -228,31 +233,29 @@ def build_summary(pairs: list[tuple[str, str]]) -> dict[str, Any]:
228233
if counts and len(set(counts)) > 1:
229234
mps = "ERROR"
230235

231-
if pairs:
232-
first = json.loads(pairs[0][1])
233-
last = json.loads(pairs[-1][1])
234-
time_block = {
235-
"begin_step": {
236-
"nanosec": int(pairs[0][0]),
237-
"begin": first.get("start"),
238-
"end": first.get("end"),
239-
},
240-
"end_step": {
241-
"nanosec": int(pairs[-1][0]),
242-
"begin": last.get("start"),
243-
"end": last.get("end"),
244-
},
245-
}
246-
else:
247-
empty = {"nanosec": None, "begin": None, "end": None}
248-
time_block = {"begin_step": empty.copy(), "end_step": empty.copy()}
236+
# Parse first and last entries (guaranteed to exist after early exit check)
237+
first = json.loads(pairs[0][1])
238+
last = json.loads(pairs[-1][1])
239+
240+
time_block = {
241+
"begin_step": {
242+
"nanosec": int(pairs[0][0]),
243+
"begin": first.get("start"),
244+
"end": first.get("end"),
245+
},
246+
"end_step": {
247+
"nanosec": int(pairs[-1][0]),
248+
"begin": last.get("start"),
249+
"end": last.get("end"),
250+
},
251+
}
249252

250253
# Get aggregated data by type
251254
by_types, total_r, qty_by_types = aggregate_rates_by_type(pairs)
252255

253256
# Get overall time range for by_type entries
254-
begin_time = first.get("start") if pairs else None
255-
end_time = last.get("end") if pairs else None
257+
begin_time = first.get("start")
258+
end_time = last.get("end")
256259

257260
# Build flat list of entries
258261
rate_list = []
@@ -292,6 +295,31 @@ def write_yaml(path: Path, doc: dict[str, Any]) -> None:
292295
)
293296

294297

298+
def _str_to_bool(value: str) -> bool:
299+
"""
300+
Convert string to boolean.
301+
302+
Args:
303+
value: String representation of boolean.
304+
305+
Returns:
306+
Boolean value.
307+
308+
Raises:
309+
argparse.ArgumentTypeError: If value cannot be converted.
310+
"""
311+
if isinstance(value, bool):
312+
return value
313+
if value.lower() in ('yes', 'true', 't', 'y', '1'):
314+
return True
315+
elif value.lower() in ('no', 'false', 'f', 'n', '0'):
316+
return False
317+
else:
318+
raise argparse.ArgumentTypeError(
319+
f"Boolean value expected. Got: {value}"
320+
)
321+
322+
295323
def main() -> None:
296324
parser = argparse.ArgumentParser(
297325
description=(
@@ -311,10 +339,12 @@ def main() -> None:
311339
)
312340
parser.add_argument(
313341
"--debug",
314-
action="store_true",
342+
type=_str_to_bool,
343+
default=False,
344+
metavar="BOOL",
315345
help=(
316346
"Enable debug mode: write <stem>_diff.txt with one "
317-
"[ts,log] JSON per line."
347+
"[ts,log] JSON per line (true/false)."
318348
),
319349
)
320350
parser.add_argument(
@@ -323,8 +353,7 @@ def main() -> None:
323353
default=None,
324354
metavar="DIR",
325355
help=(
326-
"Directory for debug output. If not specified, uses the "
327-
"directory from -o output path."
356+
"Directory for debug output. Required when --debug is enabled."
328357
),
329358
)
330359
args = parser.parse_args()
@@ -338,9 +367,14 @@ def main() -> None:
338367
pairs = extract_and_sort(args.json)
339368

340369
if args.debug:
341-
# Determine debug directory: use --debug_dir if provided,
342-
# otherwise use output directory
343-
debug_dir = args.debug_dir if args.debug_dir else out_path.parent
370+
# Require debug directory when debug mode is enabled
371+
if not args.debug_dir:
372+
print(
373+
"Error: --debug_dir is required when --debug is enabled",
374+
file=sys.stderr
375+
)
376+
sys.exit(1)
377+
debug_dir = args.debug_dir
344378
debug_dir.mkdir(parents=True, exist_ok=True)
345379
dbg_file = debug_dir / f"{args.json.stem}_diff.txt"
346380
with dbg_file.open("w", encoding="utf-8") as f:
@@ -350,13 +384,6 @@ def main() -> None:
350384
doc = build_summary(pairs)
351385
write_yaml(out_path, doc)
352386

353-
if doc["data_summary"]["metrics_per_step"] == "ERROR":
354-
per_ts = Counter(ts for ts, _ in pairs)
355-
exp = next(iter(per_ts.values()), 0)
356-
for ts in sorted(per_ts, key=int):
357-
if per_ts[ts] != exp:
358-
print(ts, per_ts[ts], file=sys.stdout)
359-
360387

361388
if __name__ == "__main__":
362389
main()

roles/telemetry_chargeback/files/gen_synth_loki_data.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def generate_loki_data(
109109
end_time: datetime,
110110
time_step_seconds: int,
111111
config: Dict[str, Any],
112+
scenario_name: str,
112113
reverse_timestamps: bool = True,
113114
):
114115
"""
@@ -121,6 +122,7 @@ def generate_loki_data(
121122
end_time (datetime): The end time for data generation.
122123
time_step_seconds (int): The duration of each log entry in seconds.
123124
config (Dict[str, Any]): Configuration dictionary loaded from file.
125+
scenario_name (str): Name of the test scenario for Loki labeling.
124126
reverse_timestamps (bool): If True, sort timestamps in descending order
125127
(newest first, oldest last). If False, sort in ascending order
126128
(oldest first, newest last). Default is True (descending).
@@ -138,6 +140,7 @@ def generate_loki_data(
138140
logger.debug(f"Time range in epoch seconds: {start_epoch} to {end_epoch}")
139141

140142
log_data_list = [] # This list will hold all our data points
143+
last_end_of_step_epoch = None # Track last entry's end epoch
141144

142145
# Loop through the time range and generate data points
143146
for current_epoch in range(
@@ -164,13 +167,13 @@ def generate_loki_data(
164167
"end_time": end_str
165168
})
166169

170+
# Track the last end epoch
171+
last_end_of_step_epoch = end_of_step_epoch
172+
167173
# Add final entry that ends at end_epoch (current time)
168-
if log_data_list and end_epoch > start_epoch:
174+
if log_data_list and end_epoch > start_epoch and last_end_of_step_epoch:
169175
# Calculate start of final entry based on end of last generated entry
170-
last_entry_end = log_data_list[-1]["end_time"]
171-
# Parse the last entry's end time to get the epoch
172-
last_end_dt = datetime.fromisoformat(last_entry_end)
173-
final_start_epoch = int(last_end_dt.timestamp()) + 1
176+
final_start_epoch = last_end_of_step_epoch + 1
174177
final_nanoseconds = int(final_start_epoch * 1_000_000_000)
175178

176179
# Only add if the final entry would have a valid duration
@@ -339,12 +342,16 @@ def tojson_preserve_order(obj):
339342

340343
log_types_list.append(log_types_with_dates)
341344

342-
# Get loki_stream configuration
345+
# Get loki_stream configuration and add scenario
343346
loki_stream = config.get("loki_stream", {})
344347
if not loki_stream:
345348
logger.warning("No loki_stream configuration found, using defaults")
346349
loki_stream = {"service": "cloudkitty"}
347350

351+
# Add scenario name to loki_stream labels
352+
loki_stream["scenario"] = scenario_name
353+
logger.info(f"Adding scenario label: {scenario_name}")
354+
348355
# Build template context with generic log type information
349356
template_context = {
350357
"log_data": log_data_list,
@@ -379,6 +386,31 @@ def tojson_preserve_order(obj):
379386
sys.exit(1)
380387

381388

389+
def _str_to_bool(value: str) -> bool:
390+
"""
391+
Convert string to boolean.
392+
393+
Args:
394+
value: String representation of boolean.
395+
396+
Returns:
397+
Boolean value.
398+
399+
Raises:
400+
argparse.ArgumentTypeError: If value cannot be converted.
401+
"""
402+
if isinstance(value, bool):
403+
return value
404+
if value.lower() in ('yes', 'true', 't', 'y', '1'):
405+
return True
406+
elif value.lower() in ('no', 'false', 'f', 'n', '0'):
407+
return False
408+
else:
409+
raise argparse.ArgumentTypeError(
410+
f"Boolean value expected. Got: {value}"
411+
)
412+
413+
382414
def main():
383415
"""Main entry point for the script."""
384416
parser = argparse.ArgumentParser(
@@ -421,8 +453,10 @@ def main():
421453
)
422454
parser.add_argument(
423455
"--debug",
424-
action="store_true",
425-
help="Enable debug level logging for verbose output."
456+
type=_str_to_bool,
457+
default=False,
458+
metavar="BOOL",
459+
help="Enable debug level logging for verbose output (true/false)."
426460
)
427461
args = parser.parse_args()
428462

@@ -435,7 +469,11 @@ def main():
435469
config = load_config(args.test)
436470
except (FileNotFoundError, ValueError) as e:
437471
logger.critical(f"Failed to load config: {e}")
438-
return
472+
sys.exit(1)
473+
474+
# Derive scenario name from test file path
475+
scenario_name = args.test.stem
476+
logger.info(f"Derived scenario name from test file: {scenario_name}")
439477

440478
# Get generation parameters from config
441479
generation_config = config.get("generation", {})
@@ -456,14 +494,17 @@ def main():
456494
end_time=end_time_utc,
457495
time_step_seconds=step_seconds,
458496
config=config,
497+
scenario_name=scenario_name,
459498
reverse_timestamps=args.reverse,
460499
)
461500
except FileNotFoundError:
462501
logger.error(
463502
"Process aborted because the template file was not found."
464503
)
504+
sys.exit(1)
465505
except Exception as e:
466506
logger.critical(f"A critical, unhandled error stopped the script: {e}")
507+
sys.exit(1)
467508

468509

469510
if __name__ == "__main__":

roles/telemetry_chargeback/tasks/cleanup_ck.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
### cleans up after each test scenario
23
- name: "Cleanup local certificates"
34
ansible.builtin.file:
45
path: "{{ local_cert_dir }}"

0 commit comments

Comments
 (0)