Skip to content

Commit 8e4179b

Browse files
authored
benchmark cleanup (#51)
* modify vms hardware * better global env variables and install templates in orchestrator * removed env variables * env var handling * template * cores * parser * documentation on how to run a benchmark
1 parent 8fcbb0c commit 8e4179b

24 files changed

Lines changed: 316 additions & 114 deletions

BENCHMARK.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Benchmark
2+
3+
## How to run benchmarks
4+
5+
**NON Production Servers only** Scripts are going to delete VMs and Templates. No questions asked.
6+
7+
```bash
8+
git clone https://github.com/egandro/proxmox-cpu-affinity
9+
cd proxmox-cpu-affinity
10+
scp -r ./benchmark proxmox:/benchmark
11+
```
12+
13+
ssh to your proxmox
14+
15+
```bash
16+
cd /benchmark
17+
18+
# customize
19+
#cp /benchmark/env.template /benchmark/.env
20+
#vi /benchmark/.env
21+
22+
# install dependencies
23+
/benchmark/scripts/install-dependencies.sh
24+
25+
# install template
26+
# dry-run
27+
#/benchmark/scripts/orchestrator.py --create-templates --dry-run -v
28+
/benchmark/scripts/orchestrator.py --create-templates
29+
30+
# show available testcases
31+
/benchmark/scripts/orchestrator.py --show
32+
33+
# run testcase
34+
# dry-run
35+
/benchmark/scripts/orchestrator.py -t helloworld --dry-run -v
36+
# run normal
37+
/benchmark/scripts/orchestrator.py -t helloworld
38+
# run verbose (print environment variables)
39+
/benchmark/scripts/orchestrator.py -t helloworld -v
40+
# run quiet (only the step names)
41+
/benchmark/scripts/orchestrator.py -t helloworld -q
42+
```
43+
44+
## Results
45+
46+
The results are stored in `/benchmark/results`.
47+
48+
A file `success` or `failed` indicated the status of the test.

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ get-status-svg:
7171

7272
deploy-benchmark-tools:
7373
ssh $(BENCHMARK_USER)@$(BENCHMARK_HOST) "mkdir -p /benchmark"
74-
ssh $(BENCHMARK_USER)@$(BENCHMARK_HOST) "[ -f /benchmark/config.sh ]" || \
75-
scp benchmark/config.sh $(BENCHMARK_USER)@$(BENCHMARK_HOST):/benchmark/config.sh
74+
if [ -f benchmark/.env ]; then \
75+
scp benchmark/.env $(BENCHMARK_USER)@$(BENCHMARK_HOST):/benchmark/.env; \
76+
else \
77+
scp benchmark/env.template $(BENCHMARK_USER)@$(BENCHMARK_HOST):/benchmark/.env; \
78+
fi
7679
#ssh $(BENCHMARK_USER)@$(BENCHMARK_HOST) "[ -f /benchmark/testcases.json ]"
7780
scp benchmark/testcases.json $(BENCHMARK_USER)@$(BENCHMARK_HOST):/benchmark/testcases.json
7881
scp -r benchmark/scripts $(BENCHMARK_USER)@$(BENCHMARK_HOST):/benchmark

benchmark/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
config.sh
1+
.env

benchmark/config.sh-template

Lines changed: 0 additions & 20 deletions
This file was deleted.

benchmark/env.template

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
########### Configuration Overrides
2+
# You can overwrite any variable from global_config here, or define additional variables.
3+
4+
########### Execution Context
5+
# When running via the orchestrator, these variables are loaded and override global defaults.
6+
# When running scripts directly (without the orchestrator), this .env file is sourced if present.
7+
8+
########### Proxmox Infrastructure (PVE)
9+
#PVE_STORAGE="local-zfs"
10+
#PVE_STORAGE_SNIPPETS="local-zfs"
11+
#PVE_VM_SSH_KEY_FILE_PUB="/root/.ssh/id_rsa.pub"
12+
#PVE_VM_SSH_KEY_FILE="/root/.ssh/id_rsa"

benchmark/orchestrator.py

Whitespace-only changes.

benchmark/scripts/create-templates.sh

Lines changed: 0 additions & 13 deletions
This file was deleted.

benchmark/scripts/install-dependencies.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ set -e
44

55
SCRIPTDIR="$(dirname "$0")"
66

7-
. ${SCRIPTDIR}/../config.sh
7+
if [ -z "$ORCHESTRATOR_MODE" ] && [ -f "${SCRIPTDIR}/../.env" ]; then
8+
. "${SCRIPTDIR}/../.env"
9+
fi
810

911
apt update -qq
10-
apt install jq -qq -y
12+
apt install jq python3-dotenv -qq -y
File renamed without changes.

benchmark/scripts/orchestrator.py

Lines changed: 105 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,39 @@
44
import os
55
import sys
66
import argparse
7+
from dotenv import dotenv_values
8+
9+
def resolve_env_vars(env_vars):
10+
"""Resolves environment variables with transitive substitution."""
11+
original_env = os.environ.copy()
12+
resolved = {k: str(v) for k, v in env_vars.items()}
13+
14+
try:
15+
# Seed os.environ with our variables so expandvars can find them
16+
os.environ.update(resolved)
17+
18+
# Multi-pass expansion to handle transitive dependencies
19+
for _ in range(10):
20+
changed = False
21+
for k, v in resolved.items():
22+
new_val = os.path.expandvars(v)
23+
if new_val != v:
24+
resolved[k] = new_val
25+
os.environ[k] = new_val
26+
changed = True
27+
if not changed:
28+
break
29+
return resolved
30+
finally:
31+
os.environ.clear()
32+
os.environ.update(original_env)
733

834
def run_command(command, env_vars, step_name, dry_run=False, verbose=False, quiet=False):
935
"""Executes a shell command with the specific environment."""
36+
# Ensure all environment variables are strings
37+
env_vars = {k: str(v) for k, v in env_vars.items()}
38+
env_vars = resolve_env_vars(env_vars)
39+
1040
display_cmd = command
1141
prefix = "[EXEC]"
1242

@@ -52,13 +82,47 @@ def run_command(command, env_vars, step_name, dry_run=False, verbose=False, quie
5282
print(f" [ERROR] Step '{step_name}' failed with exit code {e.returncode}")
5383
raise e
5484

85+
def execute_scripts(scripts, env, context_name, args):
86+
"""Iterates over scripts and executes them."""
87+
for step_name, step_config in scripts.items():
88+
# Merge Step-specific Env
89+
step_env = env.copy()
90+
step_env.update(step_config.get("env", {}))
91+
92+
# Handle "cmds" (list) vs "cmd" (string)
93+
commands = []
94+
if "cmds" in step_config:
95+
commands = step_config["cmds"]
96+
elif "cmd" in step_config:
97+
commands = [step_config["cmd"]]
98+
99+
# Execute
100+
try:
101+
for cmd in commands:
102+
run_command(cmd, step_env, step_name, dry_run=args.dry_run, verbose=args.verbose, quiet=args.quiet)
103+
except subprocess.CalledProcessError:
104+
print(f"Aborting '{context_name}' due to failure in step '{step_name}'.")
105+
return False
106+
return True
107+
55108
def main():
56109
parser = argparse.ArgumentParser(description="Benchmark Orchestrator")
57-
parser.add_argument("config", nargs="?", default="/benchmark/testcases.json", help="Path to configuration file")
110+
parser.add_argument("config", nargs="?", default="/benchmark/testcases.json", help="Path to configuration file (default: %(default)s)")
111+
112+
mode_group = parser.add_mutually_exclusive_group(required=True)
113+
mode_group.add_argument("-t", "--testcase", help="Only run the specified testcase")
114+
mode_group.add_argument("--all", action="store_true", help="Run all testcases")
115+
mode_group.add_argument("--create-templates", action="store_true", help="Run template creation scripts")
116+
mode_group.add_argument("--show", action="store_true", help="List available testcases")
117+
58118
parser.add_argument("--dry-run", action="store_true", help="Simulate execution")
59119
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
60120
parser.add_argument("-q", "--quiet", action="store_true", help="Suppress command output")
61-
parser.add_argument("-t", "--testcase", help="Only run the specified testcase")
121+
122+
if len(sys.argv) == 1:
123+
parser.print_help(sys.stderr)
124+
sys.exit(1)
125+
62126
args = parser.parse_args()
63127

64128
if not os.path.exists(args.config):
@@ -68,48 +132,58 @@ def main():
68132
with open(args.config, 'r') as f:
69133
data = json.load(f)
70134

71-
global_env = data.get("global_config", {}).get("env", {})
135+
if args.show:
136+
print("Available Testcases:")
137+
for tc in data.get("testcases", []):
138+
print(f" - {tc.get('name', 'unnamed')}")
139+
sys.exit(0)
72140

73-
for testcase in data.get("testcases", []):
74-
if "name" not in testcase:
141+
global_env = data.get("global_config", {}).get("env", {})
142+
global_env["ORCHESTRATOR_MODE"] = "1"
143+
144+
# Load .env from ../.env relative to script location
145+
script_dir = os.path.dirname(os.path.abspath(__file__))
146+
env_path = os.path.join(script_dir, "..", ".env")
147+
if os.path.exists(env_path):
148+
shell_env = dotenv_values(env_path)
149+
global_env.update(shell_env)
150+
151+
# Determine execution targets
152+
if args.create_templates:
153+
if "create_templates" not in data:
154+
print("Warning: 'create_templates' section not found in configuration.")
155+
targets = []
156+
else:
157+
targets = [data["create_templates"]]
158+
else:
159+
targets = data.get("testcases", [])
160+
161+
for testcase in targets:
162+
if "name" in testcase:
163+
context_name = testcase["name"]
164+
elif args.create_templates:
165+
context_name = "create_templates"
166+
else:
75167
print("Error: Testcase definition missing required 'name' field.")
76168
sys.exit(1)
77169

78-
tc_name = testcase["name"]
79-
if args.testcase and args.testcase != tc_name:
170+
if not args.create_templates and args.testcase and args.testcase != context_name:
80171
continue
81172

82173
print(f"\n========================================")
83-
print(f"RUNNING TESTCASE: {tc_name}")
174+
print(f"RUNNING: {context_name}")
84175
print(f"========================================")
85176

86-
# 1. Setup Base Environment (Global + Testcase)
177+
# Setup Environment
87178
tc_env = global_env.copy()
88-
tc_env["TESTCASE"] = tc_name
179+
if not args.create_templates:
180+
tc_env["TESTCASE"] = context_name
89181
tc_env.update(testcase.get("env", {}))
90182

91183
scripts = testcase.get("scripts", {})
92-
93-
# 2. Iterate EXACTLY as defined in the JSON file
94-
for step_name, step_config in scripts.items():
95-
# Merge Step-specific Env
96-
step_env = tc_env.copy()
97-
step_env.update(step_config.get("env", {}))
98-
99-
# Handle "cmds" (list) vs "cmd" (string)
100-
commands = []
101-
if "cmds" in step_config:
102-
commands = step_config["cmds"]
103-
elif "cmd" in step_config:
104-
commands = [step_config["cmd"]]
105-
106-
# Execute
107-
try:
108-
for cmd in commands:
109-
run_command(cmd, step_env, step_name, dry_run=args.dry_run, verbose=args.verbose, quiet=args.quiet)
110-
except subprocess.CalledProcessError:
111-
print(f"Aborting testcase '{tc_name}' due to failure in step '{step_name}'.")
112-
break
184+
if not execute_scripts(scripts, tc_env, context_name, args):
185+
if args.create_templates:
186+
sys.exit(1)
113187

114188
if __name__ == "__main__":
115189
main()

0 commit comments

Comments
 (0)