Skip to content

Commit 2a772dd

Browse files
Add RKE2 reposync storage benchmark
Add an opt-in Uyuni testsuite benchmark for RKE2 storage using the real spacewalk-repo-sync workflow against a mounted local file:// repository. The benchmark verifies that the source repository is mounted in the Uyuni server pod, creates a unique software channel through the Uyuni API, runs spacewalk-repo-sync inside the server pod, and stores raw logs and a structured summary under: /var/spacewalk/uyuni-bench/results/reposync/<backend>-<timestamp>/ The summary includes workload metadata, source and imported package counts, exit code, storage backend, raw log paths, parsed package and patch metrics, phase timings, throughput values, and sync status. The parser supports full and short reposync timestamps, multiple-run logs, container output noise, missing /usr/bin/time fallback, package import metrics, channel linking metrics, and patch sync metrics.
1 parent 6d07b98 commit 2a772dd

5 files changed

Lines changed: 551 additions & 0 deletions

File tree

testsuite/config/cucumber.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ salt: --tags @scope_salt
3535
software_channels_and_repositories: --tags @scope_software_channels_and_repositories
3636
spacecmd: --tags @scope_spacecmd
3737
spacewalk_utils: --tags @scope_spacewalk_utils
38+
storage_benchmark: --tags @scope_storage_benchmark
3839
sp_migration: --tags @scope_sp_migration
3940
subscription_matching: --tags @scope_subscription_matching
4041
sumatoolbox: --tags @scope_sumatoolbox
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright (c) 2026 SUSE LLC
2+
# Licensed under the terms of the MIT license.
3+
#
4+
5+
@rke2
6+
@scope_storage_benchmark
7+
@benchmark
8+
Feature: RKE2 storage benchmark
9+
In order to compare storage backends for Uyuni on Kubernetes
10+
As the system administrator
11+
I want to run a real Uyuni repository synchronization workload
12+
13+
Scenario: Run a local file repository synchronization benchmark
14+
Given The Kubernetes cluster is ready on "server"
15+
And the reposync benchmark source repository is mounted in the server pod
16+
When I create a unique reposync benchmark channel
17+
And I run the reposync benchmark for the mounted source repository
18+
Then the reposync benchmark should finish successfully
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Copyright (c) 2026 SUSE LLC.
2+
# Licensed under the terms of the MIT license.
3+
4+
### Step definitions for optional real-world storage benchmark workloads.
5+
6+
require 'base64'
7+
require 'json'
8+
require 'shellwords'
9+
require 'time'
10+
11+
require_relative '../support/storage_benchmark_parser'
12+
13+
UYUNI_BENCH_DEFAULT_SOURCE_REPO = '/uyuni-bench-source/leap-15.6-backports-update'.freeze
14+
UYUNI_BENCH_DEFAULT_RESULTS_PARENT = '/var/spacewalk/uyuni-bench/results/reposync'.freeze
15+
16+
# Return the Kubernetes server pod running Uyuni.
17+
def reposync_benchmark_server_pod
18+
return @reposync_benchmark_server_pod if @reposync_benchmark_server_pod
19+
20+
pod = get_pod_name('server', 'server')
21+
raise ScriptError, 'Unable to determine the Uyuni server pod name' if pod.nil? || pod.empty?
22+
23+
@reposync_benchmark_server_pod = pod
24+
end
25+
26+
# Run a shell command on the RKE2 host.
27+
def reposync_benchmark_run_on_host(command, timeout: DEFAULT_TIMEOUT, verbose: true, check_errors: true)
28+
get_target('server').run_local(command, timeout: timeout, verbose: verbose, check_errors: check_errors)
29+
end
30+
31+
# Run a shell command inside the Uyuni server pod.
32+
def reposync_benchmark_run_in_server_pod(command, timeout: DEFAULT_TIMEOUT, verbose: true, check_errors: true)
33+
pod = Shellwords.escape(reposync_benchmark_server_pod)
34+
escaped_command = Shellwords.escape(command)
35+
reposync_benchmark_run_on_host(
36+
"kubectl -n uyuni exec #{pod} -- sh -lc #{escaped_command}",
37+
timeout: timeout,
38+
verbose: verbose,
39+
check_errors: check_errors
40+
)
41+
end
42+
43+
# Path to the mounted source repository as seen by the Uyuni server container.
44+
def reposync_benchmark_source_repo
45+
ENV.fetch('UYUNI_BENCH_SOURCE_REPO', UYUNI_BENCH_DEFAULT_SOURCE_REPO).delete_suffix('/')
46+
end
47+
48+
# URL passed to spacewalk-repo-sync.
49+
def reposync_benchmark_source_url
50+
"file://#{reposync_benchmark_source_repo}/"
51+
end
52+
53+
# Storage backend label stored in the benchmark summary.
54+
def reposync_benchmark_storage_backend
55+
ENV.fetch('UYUNI_BENCH_STORAGE_BACKEND', ENV.fetch('STORAGE_BACKEND', 'unknown'))
56+
end
57+
58+
# Parse an integer environment variable and raise a clear error for invalid values.
59+
def reposync_benchmark_integer_env(name, default, minimum:)
60+
value = ENV.fetch(name, default).to_s
61+
valid_value = value.match?(/\A\d+\z/) && value.to_i >= minimum
62+
raise ScriptError, "#{name} must be an integer >= #{minimum}, got #{value.inspect}" unless valid_value
63+
64+
value.to_i
65+
end
66+
67+
# Directory for this benchmark run as seen by the Uyuni server container.
68+
def reposync_benchmark_results_dir
69+
return @reposync_benchmark_results_dir if @reposync_benchmark_results_dir
70+
71+
@reposync_benchmark_results_dir =
72+
if ENV['UYUNI_BENCH_RESULTS_DIR']
73+
ENV.fetch('UYUNI_BENCH_RESULTS_DIR').delete_suffix('/')
74+
else
75+
timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
76+
File.join(UYUNI_BENCH_DEFAULT_RESULTS_PARENT, "#{reposync_benchmark_storage_backend}-#{timestamp}")
77+
end
78+
end
79+
80+
# Unique channel label for this benchmark run.
81+
def reposync_benchmark_channel_label
82+
return @reposync_benchmark_channel_label if @reposync_benchmark_channel_label
83+
84+
@reposync_benchmark_channel_label =
85+
ENV.fetch('UYUNI_BENCH_CHANNEL_LABEL') do
86+
"uyuni-bench-reposync-#{Time.now.utc.strftime('%Y%m%d%H%M%S')}"
87+
end
88+
end
89+
90+
# Write JSON content into the server pod without depending on host path sharing.
91+
def reposync_benchmark_write_json_in_server_pod(path, payload)
92+
encoded = Base64.strict_encode64(JSON.pretty_generate(payload))
93+
reposync_benchmark_run_in_server_pod(
94+
"mkdir -p #{Shellwords.escape(File.dirname(path))} && " \
95+
"printf '%s' #{Shellwords.escape(encoded)} | base64 -d > #{Shellwords.escape(path)}"
96+
)
97+
end
98+
99+
Given('the reposync benchmark source repository is mounted in the server pod') do
100+
repo = Shellwords.escape(reposync_benchmark_source_repo)
101+
command = "test -r #{repo}/repodata/repomd.xml && find #{repo} -name '*.rpm' | wc -l"
102+
output, = reposync_benchmark_run_in_server_pod(command, timeout: 300)
103+
@reposync_benchmark_source_package_count = reposync_benchmark_last_integer(output)
104+
105+
raise ScriptError, "No RPM packages found in #{reposync_benchmark_source_repo}" if @reposync_benchmark_source_package_count.zero?
106+
107+
log "Reposync benchmark source: #{reposync_benchmark_source_url}"
108+
log "Reposync benchmark source RPMs: #{@reposync_benchmark_source_package_count}"
109+
end
110+
111+
When('I create a unique reposync benchmark channel') do
112+
label = reposync_benchmark_channel_label
113+
name = label
114+
summary = 'Uyuni reposync storage benchmark channel'
115+
arch = ENV.fetch('UYUNI_BENCH_CHANNEL_ARCH', 'channel-x86_64')
116+
parent = ENV.fetch('UYUNI_BENCH_PARENT_CHANNEL', '')
117+
118+
assert_equal(1, $api_test.channel.software.create(label, name, summary, arch, parent))
119+
log "Created reposync benchmark channel: #{label}"
120+
end
121+
122+
When('I run the reposync benchmark for the mounted source repository') do
123+
@reposync_benchmark_summary_path = File.join(reposync_benchmark_results_dir, 'summary.json')
124+
@reposync_benchmark_stdout_path = File.join(reposync_benchmark_results_dir, 'spacewalk-repo-sync.stdout.log')
125+
@reposync_benchmark_stderr_path = File.join(reposync_benchmark_results_dir, 'spacewalk-repo-sync.stderr.log')
126+
@reposync_benchmark_time_path = File.join(reposync_benchmark_results_dir, 'time.verbose.log')
127+
128+
channel = Shellwords.escape(reposync_benchmark_channel_label)
129+
source_url = Shellwords.escape(reposync_benchmark_source_url)
130+
results_dir = Shellwords.escape(reposync_benchmark_results_dir)
131+
stdout_path = Shellwords.escape(@reposync_benchmark_stdout_path)
132+
stderr_path = Shellwords.escape(@reposync_benchmark_stderr_path)
133+
time_path = Shellwords.escape(@reposync_benchmark_time_path)
134+
timeout = reposync_benchmark_integer_env('UYUNI_BENCH_REPOSYNC_TIMEOUT', '14400', minimum: 1)
135+
136+
sync_command = "spacewalk-repo-sync -c #{channel} --url=#{source_url}"
137+
command = "mkdir -p #{results_dir} && " \
138+
'if [ -x /usr/bin/time ]; then ' \
139+
"/usr/bin/time -v -o #{time_path} #{sync_command}; " \
140+
"else #{sync_command}; fi " \
141+
"> #{stdout_path} 2> #{stderr_path}"
142+
143+
started_at = Time.now.utc
144+
started_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
145+
_output, @reposync_benchmark_exit_code = reposync_benchmark_run_in_server_pod(
146+
command,
147+
timeout: timeout,
148+
check_errors: false
149+
)
150+
finished_at = Time.now.utc
151+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_monotonic
152+
153+
time_output, = reposync_benchmark_run_in_server_pod(
154+
"test -f #{time_path} && cat #{time_path} || true",
155+
verbose: false,
156+
check_errors: false
157+
)
158+
sync_stdout, = reposync_benchmark_run_in_server_pod(
159+
"test -f #{stdout_path} && cat #{stdout_path} || true",
160+
verbose: false,
161+
check_errors: false
162+
)
163+
sync_stderr, = reposync_benchmark_run_in_server_pod(
164+
"test -f #{stderr_path} && cat #{stderr_path} || true",
165+
verbose: false,
166+
check_errors: false
167+
)
168+
reposync_log_metrics = reposync_benchmark_parse_reposync_log([sync_stdout, sync_stderr].join("\n"))
169+
170+
@reposync_benchmark_package_count_error = nil
171+
begin
172+
@reposync_benchmark_imported_package_count = $api_test.call(
173+
'channel.software.listAllPackages',
174+
sessionKey: $api_test.token,
175+
channelLabel: reposync_benchmark_channel_label
176+
).length
177+
rescue StandardError => e
178+
@reposync_benchmark_imported_package_count = 0
179+
@reposync_benchmark_package_count_error = e.message
180+
end
181+
182+
summary = {
183+
workload: 'spacewalk_repo_sync',
184+
started_at: started_at.iso8601,
185+
finished_at: finished_at.iso8601,
186+
duration_seconds: duration.round(3),
187+
exit_code: @reposync_benchmark_exit_code,
188+
storage_backend: reposync_benchmark_storage_backend,
189+
channel_label: reposync_benchmark_channel_label,
190+
source_url: reposync_benchmark_source_url,
191+
source_package_count: @reposync_benchmark_source_package_count,
192+
imported_package_count: @reposync_benchmark_imported_package_count,
193+
package_count_error: @reposync_benchmark_package_count_error,
194+
stdout_path: @reposync_benchmark_stdout_path,
195+
stderr_path: @reposync_benchmark_stderr_path,
196+
time_verbose_path: @reposync_benchmark_time_path,
197+
time_metrics: reposync_benchmark_parse_time_metrics(time_output),
198+
reposync_log_metrics: reposync_log_metrics
199+
}
200+
201+
reposync_benchmark_write_json_in_server_pod(@reposync_benchmark_summary_path, summary)
202+
log "Reposync benchmark summary: #{@reposync_benchmark_summary_path}"
203+
end
204+
205+
Then('the reposync benchmark should finish successfully') do
206+
raise ScriptError, 'Reposync benchmark did not run' if @reposync_benchmark_exit_code.nil?
207+
raise ScriptError, "spacewalk-repo-sync failed with exit code #{@reposync_benchmark_exit_code}" unless @reposync_benchmark_exit_code.zero?
208+
209+
minimum_packages = reposync_benchmark_integer_env(
210+
'UYUNI_BENCH_MIN_PACKAGES',
211+
@reposync_benchmark_source_package_count.to_s,
212+
minimum: 0
213+
)
214+
imported = @reposync_benchmark_imported_package_count.to_i
215+
raise ScriptError, "Expected at least #{minimum_packages} imported packages, got #{imported}" if imported < minimum_packages
216+
217+
summary = Shellwords.escape(@reposync_benchmark_summary_path)
218+
_output, code = reposync_benchmark_run_in_server_pod("test -s #{summary}", check_errors: false, verbose: false)
219+
raise ScriptError, "Reposync benchmark summary not found at #{@reposync_benchmark_summary_path}" unless code.zero?
220+
221+
log "Reposync benchmark channel: #{reposync_benchmark_channel_label}"
222+
log "Reposync benchmark imported packages: #{imported}"
223+
end

0 commit comments

Comments
 (0)