Skip to content

Commit 8bb17cf

Browse files
myadlaayefimov-1
authored andcommitted
Add synthetic data generation files and changes
Gemini AI used
1 parent fc83d27 commit 8bb17cf

File tree

11 files changed

+403
-23
lines changed

11 files changed

+403
-23
lines changed

.zuul.yaml

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,27 @@
156156
irrelevant-files: *irrelevant_files
157157
required-projects: *required_projects
158158

159+
- job:
160+
name: functional-chargeback-tests-osp18
161+
parent: telemetry-operator-multinode-cloudkitty
162+
description: |
163+
Alias of telemetry-operator-multinode-cloudkitty for testing
164+
irrelevant-files: []
165+
roles:
166+
- zuul: github.com/openstack-k8s-operators/ci-framework
167+
- zuul: github.com/infrawatch/feature-verification-tests
168+
required-projects:
169+
- name: github.com/openstack-k8s-operators/telemetry-operator
170+
override-checkout: main
171+
- name: github.com/infrawatch/feature-verification-tests
172+
vars:
173+
cifmw_extras:
174+
- "@{{ ansible_user_dir }}/{{ zuul.projects['github.com/openstack-k8s-operators/ci-framework'].src_dir }}/scenarios/centos-9/multinode-ci.yml"
175+
# Need a config for CK
176+
- "@{{ ansible_user_dir }}/{{ zuul.projects['github.com/openstack-k8s-operators/telemetry-operator'].src_dir }}/ci/vars-cloudkitty-tempest.yml"
177+
- "@{{ ansible_user_dir }}/{{ zuul.projects['github.com/infrawatch/feature-verification-tests'].src_dir }}/ci/vars-use-master-containers.yml"
178+
- "@{{ ansible_user_dir }}/{{ zuul.projects['github.com/infrawatch/feature-verification-tests'].src_dir }}/ci/vars-cloudkitty-fvt.yml"
179+
159180
- project:
160181
name: infrawatch/feature-verification-tests
161182
periodic:
@@ -167,26 +188,7 @@
167188
jobs:
168189
- telemetry-openstack-meta-content-provider-master:
169190
override-checkout: main
170-
- feature-verification-tests-noop:
171-
files: *irrelevant_files
172-
- functional-tests-osp18
173-
- functional-logging-tests-osp18:
174-
irrelevant-files: *irrelevant_files
175-
files:
176-
- roles/telemetry_logging/.*
177-
- roles/common/*
178-
- .zuul.yaml
179-
- ci/vars-logging-test.yml
180-
- ci/logging_tests_all.yml
181-
- ci/logging_tests_computes.yml
182-
- ci/logging_tests_controller.yml
183-
- ci/report_result.yml
184-
- .zuul.yaml
185-
- functional-periodic-telemetry-with-ceph:
186-
files:
187-
# Run this job for changes to the volume_pool_metrics test changes as this is
188-
# the only job, where those tests get executed and for changes to zuul.yaml in
189-
# case the job definition changes.
190-
- roles/telemetry_verify_metrics/tasks/verify_ceilometer_volume_pool_metrics.yml
191-
- .zuul.yaml
192-
191+
- functional-chargeback-tests-osp18:
192+
override-checkout: main
193+
dependencies:
194+
- telemetry-openstack-meta-content-provider-master

ci/run_chargeback_tests.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
- name: "Verify all the applicable projects, endpoints, pods & services for cloudkitty"
3+
hosts: "{{ cifmw_target_hook_host | default('localhost') }}"
4+
gather_facts: no
5+
ignore_errors: true
6+
environment:
7+
KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
8+
PATH: "{{ cifmw_path }}"
9+
vars_files:
10+
- vars/osp18_env.yml
11+
vars:
12+
common_pod_status_str: "Running"
13+
common_pod_nspace: openstack
14+
common_pod_list:
15+
- cloudkitty-api
16+
- cloudkitty-lokistack-compactor
17+
- cloudkitty-lokistack-distributor
18+
# pod tests expect only one instance of a pod, there are two of this one
19+
# Some work is needed on the pod tests to deal with this test case
20+
#- cloudkitty-lokistack-gateway
21+
- cloudkitty-lokistack-index-gateway
22+
- cloudkitty-lokistack-ingester
23+
- cloudkitty-lokistack-querier
24+
- cloudkitty-lokistack-query-frontend
25+
- cloudkitty-proc
26+
27+
common_project_list:
28+
- openstack
29+
- openstack-operators
30+
31+
common_endpoint_list:
32+
- [cloudkitty,rating,public]
33+
- [cloudkitty,rating,internal]
34+
35+
common_service_nspace: openstack
36+
common_service_list:
37+
- cloudkitty-internal
38+
- cloudkitty-lokistack-compactor-grpc
39+
- cloudkitty-lokistack-compactor-http
40+
- cloudkitty-lokistack-distributor-grpc
41+
- cloudkitty-lokistack-distributor-http
42+
- cloudkitty-lokistack-gateway-http
43+
- cloudkitty-lokistack-gossip-ring
44+
- cloudkitty-lokistack-index-gateway-grpc
45+
- cloudkitty-lokistack-index-gateway-http
46+
- cloudkitty-lokistack-ingester-grpc
47+
- cloudkitty-lokistack-ingester-http
48+
- cloudkitty-lokistack-querier-grpc
49+
- cloudkitty-lokistack-querier-http
50+
- cloudkitty-lokistack-query-frontend-grpc
51+
- cloudkitty-lokistack-query-frontend-http
52+
- cloudkitty-public
53+
54+
tasks:
55+
- name: "Verify cloudkitty infrastructure components"
56+
ansible.builtin.import_role:
57+
name: common
58+
59+
- name: "Verify cloudkitty deployment"
60+
ansible.builtin.import_role:
61+
name: telemetry_chargeback

ci/vars-cloudkitty-fvt.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pre_tests_01_run_chargeback_tests:
3+
source: "{{ ansible_user_dir }}/{{ zuul.projects['github.com/infrawatch/feature-verification-tests'].src_dir }}/ci/run_chargeback_tests.yml"
4+
type: playbook
5+
config_file: "{{ ansible_user_dir }}/{{ zuul.projects['github.com/infrawatch/feature-verification-tests'].src_dir }}/ci/ansible.cfg"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
telemetry_chargeback
2+
=========
3+
The **`telemetry_chargeback`** role is designed to test the **RHOSO Cloudkitty** feature. These tests are specific to the Cloudkitty feature. Tests that are not specific to this feature (e.g., standard OpenStack deployment validation, basic networking) should be added to a common role.
4+
5+
Requirements
6+
------------
7+
It relies on the following being available on the target or control host:
8+
9+
* This role requires **Ansible 2.9** or newer.
10+
* The **OpenStack CLI client** must be installed and configured with administrative credentials.
11+
* Required Python libraries for the `openstack` CLI (e.g., `python3-openstackclient`).
12+
* Connectivity to the OpenStack API endpoint.
13+
14+
It is expected to be run **after** a successful deployment and configuration of the following components:
15+
16+
* **OpenStack:** A functional OpenStack cloud (RHOSO) environment.
17+
* **Cloudkitty:** The Cloudkitty service must be installed, configured, and running.
18+
19+
Role Variables
20+
--------------
21+
The role uses a few primary variables to control the testing environment and execution.
22+
23+
| Variable | Default Value | Description |
24+
|----------|---------------|-------------|
25+
| `openstack_cmd` | `openstack` | The command used to execute OpenStack CLI calls. This can be customized if the binary is not in the standard PATH. |
26+
27+
Dependencies
28+
------------
29+
This role has no direct hard dependencies on other Ansible roles.
30+
31+
Example Playbook
32+
----------------
33+
Each tasks/playbook.yml should be called independently via "ansible.builtin.import_role" with appropriate vars passed:
34+
35+
```yaml
36+
- name: "Run chargeback tests"
37+
hosts: controllers
38+
gather_facts: no
39+
40+
tasks:
41+
- name: "Run chargeback specific tests"
42+
ansible.builtin.import_role:
43+
name: telemetry_chargeback
44+
```
45+
46+
Author Information
47+
------------------
48+
49+
Alex Yefimov, Red Hat
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
openstack_cmd: "openstack"
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import logging
2+
import argparse
3+
from datetime import datetime, timezone, timedelta
4+
from pathlib import Path
5+
from typing import Union
6+
from jinja2 import Template
7+
8+
# --- Configure logging with a default level that can be changed ---
9+
logging.basicConfig(
10+
level=logging.INFO,
11+
format='%(asctime)s - %(levelname)s - %(message)s',
12+
datefmt='%Y-%m-%d %H:%M:%S'
13+
)
14+
logger = logging.getLogger()
15+
16+
def _format_timestamp(epoch_seconds: float) -> str:
17+
"""
18+
Converts an epoch timestamp into a human-readable UTC string.
19+
20+
Args:
21+
epoch_seconds (float): The timestamp in seconds since the epoch.
22+
23+
Returns:
24+
str: The formatted datetime string (e.g., "2023-10-26T14:30:00 UTC").
25+
"""
26+
try:
27+
dt_object = datetime.fromtimestamp(epoch_seconds, tz=timezone.utc)
28+
return dt_object.strftime("%Y-%m-%dT%H:%M:%S %Z")
29+
except (ValueError, TypeError):
30+
logger.warning(f"Invalid epoch value provided: {epoch_seconds}")
31+
return "INVALID_TIMESTAMP"
32+
33+
def generate_loki_data(
34+
template_path: Path,
35+
output_path: Path,
36+
start_time: datetime,
37+
end_time: datetime,
38+
time_step_seconds: int
39+
):
40+
"""
41+
Generates synthetic Loki log data by first preparing a data list
42+
and then rendering it with a single template.
43+
44+
Args:
45+
template_path (Path): Path to the main log template file.
46+
output_path (Path): Path for the generated output JSON file.
47+
start_time (datetime): The start time for data generation.
48+
end_time (datetime): The end time for data generation.
49+
time_step_seconds (int): The duration of each log entry in seconds.
50+
"""
51+
52+
# --- Step 1: Generate the data structure first ---
53+
logger.info(
54+
f"Generating data from {start_time.strftime('%Y-%m-%d')} to "
55+
f"{end_time.strftime('%Y-%m-%d')} with a {time_step_seconds}s step."
56+
)
57+
start_epoch = int(start_time.timestamp())
58+
end_epoch = int(end_time.timestamp())
59+
logger.debug(f"Time range in epoch seconds: {start_epoch} to {end_epoch}")
60+
61+
log_data_list = [] # This list will hold all our data points
62+
63+
# Loop through the time range and generate data points
64+
for current_epoch in range(start_epoch, end_epoch, time_step_seconds):
65+
end_of_step_epoch = current_epoch + time_step_seconds - 1
66+
67+
# Prepare replacement values
68+
nanoseconds = int(current_epoch * 1_000_000_000)
69+
start_str = _format_timestamp(current_epoch)
70+
end_str = _format_timestamp(end_of_step_epoch)
71+
72+
logger.debug(f"Processing epoch: {current_epoch} -> nanoseconds: {nanoseconds}")
73+
74+
# Create a dictionary for this time step and add it to the list
75+
log_data_list.append({
76+
"nanoseconds": nanoseconds,
77+
"start_time": start_str,
78+
"end_time": end_str
79+
})
80+
81+
logger.info(f"Generated {len(log_data_list)} data points to be rendered.")
82+
83+
# --- Step 2: Load template and render ---
84+
try:
85+
logger.info(f"Loading main template from: {template_path}")
86+
template_content = template_path.read_text()
87+
template = Template(template_content, trim_blocks=True, lstrip_blocks=True)
88+
89+
except FileNotFoundError as e:
90+
logger.error(f"Error loading template file: {e}. Aborting.")
91+
raise # Re-raise the exception to be caught in main()
92+
93+
# --- Render the template in one pass with all the data ---
94+
logger.info("Rendering final output...")
95+
# The template expects a variable named 'log_data'
96+
final_output = template.render(log_data=log_data_list)
97+
98+
# --- Step 3: Write the final string to the file ---
99+
try:
100+
with output_path.open('w') as f_out:
101+
f_out.write(final_output)
102+
logger.info(f"Successfully generated synthetic data to '{output_path}'")
103+
except IOError as e:
104+
logger.error(f"Failed to write to output file '{output_path}': {e}")
105+
except Exception as e:
106+
logger.error(f"An unexpected error occurred during file write: {e}")
107+
108+
def main():
109+
"""Main entry point for the script."""
110+
parser = argparse.ArgumentParser(
111+
description="Generate synthetic Loki log data from a single main template.",
112+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
113+
)
114+
# --- Required File Path Arguments ---
115+
parser.add_argument("-o", "--output", required=True, help="Path to the output file.")
116+
# --- Only one template argument is needed now ---
117+
parser.add_argument("--template", required=True, help="Path to the main log template file (e.g., loki_main.tmpl).")
118+
119+
# --- Optional Generation Arguments ---
120+
parser.add_argument("--days", type=int, default=30, help="How many days of data to generate, ending today.")
121+
parser.add_argument("--step", type=int, default=300, help="Time step in seconds for each log entry.")
122+
123+
# --- Optional Utility Arguments ---
124+
parser.add_argument("--debug", action="store_true", help="Enable debug level logging for verbose output.")
125+
126+
args = parser.parse_args()
127+
128+
if args.debug:
129+
logger.setLevel(logging.DEBUG)
130+
logger.debug("Debug mode enabled.")
131+
132+
# Define the time range for data generation
133+
end_time_utc = datetime.now(timezone.utc)
134+
start_time_utc = end_time_utc - timedelta(days=args.days)
135+
logger.debug(f"Time range calculated: {start_time_utc} to {end_time_utc}")
136+
137+
# Run the generator
138+
try:
139+
generate_loki_data(
140+
template_path=Path(args.template),
141+
output_path=Path(args.output),
142+
start_time=start_time_utc,
143+
end_time=end_time_utc,
144+
time_step_seconds=args.step
145+
)
146+
except FileNotFoundError:
147+
logger.error("Process aborted because the template file was not found.")
148+
except Exception as e:
149+
logger.critical(f"A critical, unhandled error stopped the script: {e}")
150+
151+
152+
if __name__ == "__main__":
153+
main()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{"streams": [{ "stream": { "service": "cloudkitty" }, "values": [
2+
{%- for item in log_data %}
3+
[
4+
"{{ item.nanoseconds }}",
5+
"{\"start\": \"{{ item.start_time }}\", \"end\": \"{{ item.end_time }}\", \"type\": \"image.size\", \"unit\": \"MiB\", \"description\": null, \"qty\": 20.6875, \"price\": 0.0206875, \"groupby\": {\"id\": \"cd65d30f-8b94-4fa3-95dc-e3b429f479b2\", \"project_id\": \"0030775de80e4d84a4fd0d73e0a1b3a7\", \"user_id\": null, \"week_of_the_year\": \"37\", \"day_of_the_year\": \"258\", \"month\": \"9\", \"year\": \"2025\"}, \"metadata\": {\"container_format\": \"bare\", \"disk_format\": \"qcow2\"}}"
6+
],
7+
[
8+
"{{ item.nanoseconds }}",
9+
"{\"start\": \"{{ item.start_time }}\", \"end\": \"{{ item.end_time }}\", \"type\": \"instance\", \"unit\": \"instance\", \"description\": null, \"qty\": 1.0, \"price\": 0.3, \"groupby\": {\"id\": \"de168c31-ed44-4a1a-a079-51bd238a91d6\", \"project_id\": \"9cf5bcfc61a24682acc448af2d062ad2\", \"user_id\": \"c29ab6e886354bbd88ee9899e62d1d40\", \"week_of_the_year\": \"37\", \"day_of_the_year\": \"258\", \"month\": \"9\", \"year\": \"2025\"}, \"metadata\": {\"flavor_name\": \"m1.tiny\", \"flavor_id\": \"1\", \"vcpus\": \"\"}}"
10+
]
11+
{#- This logic adds a comma after every pair, *except* for the very last one. #}
12+
{%- if not loop.last -%}
13+
,
14+
{%- endif -%}
15+
{%- endfor %}
16+
]}]}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
galaxy_info:
3+
author: Alex Yefimov
4+
description: Tests the chargeback feature is set up in OpenStack running on OpenShift
5+
company: Red Hat
6+
7+
license: Apache-2.0
8+
9+
min_ansible_version: "2.1"
10+
11+
galaxy_tags: []
12+
13+
dependencies: []

0 commit comments

Comments
 (0)