|
| 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