# Claude AI
build-2f*
+
+# Profiling results
+prof-results/
### A specific test
```bash
-make test.multi-server.proxy-accept.short.ci
+make test.multi-server.proxy-accept.short_ci
```
### Parallel execution
- `*.test.yml` - Test parameter file (discovered by `make test.multi-server`)
- `*.ci.test.yml` - CI test parameter file (also discovered by `make test.multi-server.ci`)
- `*.yml.j2` - Jinja2 template (rendered, not treated as a test)
+
+# Multi-Server Profiling Tests
+
+Four suites instrument FreeRADIUS under Valgrind to collect heap and call-graph profiles:
+
+| Suite | What it exercises |
+| --- | --- |
+| `prof-accept` | Plain RADIUS accept (no external services) |
+| `prof-pap-auth` | PAP authentication |
+| `prof-ldap` | Authentication backed by an LDAP server |
+| `prof-mysql` | Authentication backed by a MySQL database |
+
+Results land in `prof-results/<suite>/<test>/<branch>/<commit>/<run-index>/`.
+
+### Docker image dependencies
+
+Profiling suites require images that are not needed by regular multi-server tests.
+The `freeradius-prof.image` target builds or verifies them automatically before any `prof-*` test runs:
+
+- `freeradius40x-build/ubuntu24:latest` — crossbuild base image
+- `freeradius4-<profile>/ubuntu24:latest` — FreeRADIUS profiling base image
+- `freeradius-prof:latest` — final multi-server profiling image (built by `build_image.sh`)
+
+The `prof-ldap` suite additionally requires:
+
+- `freeradius4/openldap-prof:latest` — built via the `openldap.image` target
+
+To build all profiling images explicitly:
+
+```bash
+make freeradius-prof.image
+make openldap.image # prof-ldap only
+```
+
+### Running on Linux
+
+Profiling tests run the same way as any other multi-server test:
+
+```bash
+make test.multi-server.prof-mysql.short_ci
+```
+
+### Running on macOS (Apple Silicon)
+
+The profiling image is based on a `crossbuild.<distro>` base image that is built for
+`linux/amd64`. On Apple Silicon you must pass `BUILD_PLATFORM=linux/amd64` so that
+Docker pulls and runs the correct platform variant:
+
+```bash
+make test.multi-server.prof-mysql.short_ci BUILD_PLATFORM=linux/amd64
+```
+
+This applies to all four profiling suites and to the image-build targets as well:
+
+```bash
+make freeradius-prof.image BUILD_PLATFORM=linux/amd64
+```
#
# Usage:
# make -f src/tests/multi-server/all.mk test.multi-server # run all tests
-# make -f src/tests/multi-server/all.mk test.multi-server.5hs-autoaccept.short # run single test
+# make -f src/tests/multi-server/all.mk test.multi-server.ci # run all ci tests
+# make -f src/tests/multi-server/all.mk test.multi-server.proxy-accept.short_ci # run single test
# make -f src/tests/multi-server/all.mk clean.test.multi-server # clean logs
#
SHELL := /bin/bash
+PROFILE ?= default-profiling
+BUILD_PLATFORM ?=
+
#
# Allow for stand-alone builds from the local directory.
#
DIR := $(abspath ${top_srcdir}/src/tests/multi-server)
OUTPUT := $(abspath $(BUILD_DIR)/tests/multi-server)
+GIT_BRANCH := $(or $(shell git -C $(top_srcdir) rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '_'),unknown-branch)
+GIT_COMMIT := $(or $(shell git -C $(top_srcdir) rev-parse --short HEAD 2>/dev/null),unknown-commit)
+PROF_RESULTS_ROOT := $(abspath $(top_srcdir)/prof-results)
+
# FIXME: We should be using packaged versions of the multi-server test framework
# instead of cloning from git.
TEST_MULTI_SERVER_GIT_REPO := https://github.com/InkbridgeNetworks/radenv.git
$$(foreach j,$$(TEST_MULTI_SERVER_JINJA_FILES.${1}.${2}),$$(eval $$(call TEST_MULTI_SERVER_RENDER,${1},${2},${3},$$j)))
+.PHONY: render.test.multi-server.${1}.${2}
+render.test.multi-server.${1}.${2}: $$(TEST_MULTI_SERVER_RENDERED.${1}.${2})
+
.PHONY: test.multi-server.${1}.${2}
test.multi-server.${1}.${2}: $$(TEST_MULTI_SERVER_RENDERED.${1}.${2})
$$(eval CMD := cd $(TEST_MULTI_SERVER_FRAMEWORK_DIR) && . .venv/bin/activate && DATA_PATH="${4}" python3 -m src.multi_server_test $(TEST_MULTI_SERVER_FLAGS) --project-name "${1}-${2}" --compose "${4}/environment.yml" --test "${4}/template.yml" --use-files --listener-dir "${4}/listener" --log-dir "${4}/logs" --output "${4}/logs/result.log")
}
endef
+#
+# TEST_MULTI_SERVER_PROF_INSTANCE - like TEST_MULTI_SERVER_INSTANCE but
+# computes PROF_RESULTS_PATH and exports it to docker compose so valgrind
+# output lands in:
+# $(PROF_RESULTS_ROOT)/<suite>/<test>/<branch>/<commit>/<run-index>
+#
+# ${1} = suite dir name
+# ${2} = test name
+# ${3} = params file path
+# ${4} = test output directory
+#
+define TEST_MULTI_SERVER_PROF_INSTANCE
+TEST_MULTI_SERVER_JINJA_FILES.${1}.${2} := $$(wildcard $$(DIR)/tests/${1}/*.j2)
+TEST_MULTI_SERVER_RENDERED.${1}.${2} := $$(patsubst $$(DIR)/tests/${1}/%.j2,${4}/%,$$(TEST_MULTI_SERVER_JINJA_FILES.${1}.${2}))
+
+$$(foreach j,$$(TEST_MULTI_SERVER_JINJA_FILES.${1}.${2}),$$(eval $$(call TEST_MULTI_SERVER_RENDER,${1},${2},${3},$$j)))
+
+.PHONY: render.test.multi-server.${1}.${2}
+render.test.multi-server.${1}.${2}: $$(TEST_MULTI_SERVER_RENDERED.${1}.${2})
+
+.PHONY: test.multi-server.${1}.${2}
+test.multi-server.${1}.${2}: $$(TEST_MULTI_SERVER_RENDERED.${1}.${2})
+ ${Q}mkdir -p "${4}/logs" "${4}/listener"
+ ${Q}echo "MULTI-SERVER-TEST test.multi-server.${1}.${2}"
+ ${Q}PROF_BASE="$(PROF_RESULTS_ROOT)/${1}/${2}/$(GIT_BRANCH)/$(GIT_COMMIT)"; \
+ EXISTING=$$$$( find "$$$$PROF_BASE" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ' ); \
+ RUN_INDEX=$$$$((EXISTING + 1)); \
+ PROF_RESULTS_PATH="$$$$PROF_BASE/$$$$RUN_INDEX"; \
+ mkdir -p "$$$$PROF_RESULTS_PATH" && \
+ echo "PROF_RESULTS_PATH: $$$$PROF_RESULTS_PATH" && \
+ cd $(TEST_MULTI_SERVER_FRAMEWORK_DIR) && . .venv/bin/activate && \
+ DATA_PATH="${4}" PROF_RESULTS_PATH="$$$$PROF_RESULTS_PATH" \
+ python3 -m src.multi_server_test $(TEST_MULTI_SERVER_FLAGS) --project-name "${1}-${2}" --compose "${4}/environment.yml" --test "${4}/template.yml" --use-files --listener-dir "${4}/listener" --log-dir "${4}/logs" --output "${4}/logs/result.log" \
+ > "${4}/logs/stdout.log" 2> "${4}/logs/stderr.log" || \
+ { \
+ echo "FAILED: test.multi-server.${1}.${2}"; \
+ for f in ${4}/logs/* ${4}/listener/*; do \
+ [ -f "$$$$f" ] || continue; \
+ echo ""; \
+ echo "=== $$$$f ==="; \
+ case "$$$$f" in \
+ */listener/*) \
+ echo "-- line-type counts --"; \
+ awk '{print $$$$1}' "$$$$f" | sort | uniq -c; \
+ echo "-- last 200 lines --"; \
+ ;; \
+ esac; \
+ tail -200 "$$$$f"; \
+ done; \
+ exit 1; \
+ }
+endef
+
#
# TEST_MULTI_SERVER - define all test instances for a suite.
#
# Discovers *.yml param files in the suite directory and generates
# render + test targets for each.
#
-# ${1} = suite dir name (e.g., 5hs-autoaccept)
+# ${1} = suite dir name (e.g. proxy-accept)
#
define TEST_MULTI_SERVER
TEST_MULTI_SERVER_PARAM_FILES.${1} := $$(wildcard $$(DIR)/tests/${1}/*.test.yml)
TEST_MULTI_SERVER_TESTS.${1} := $$(foreach p,$$(TEST_MULTI_SERVER_PARAM_FILES.${1}),test.multi-server.${1}.$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))))
-$$(foreach p,$$(TEST_MULTI_SERVER_PARAM_FILES.${1}),$$(eval $$(call TEST_MULTI_SERVER_INSTANCE,${1},$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))),$$p,$(OUTPUT)/${1}/$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))))))
+$$(foreach p,$$(TEST_MULTI_SERVER_PARAM_FILES.${1}),$$(eval $$(call $(if $(filter prof-%,${1}),TEST_MULTI_SERVER_PROF_INSTANCE,TEST_MULTI_SERVER_INSTANCE),${1},$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))),$$p,$(OUTPUT)/${1}/$$(subst .,_,$$(patsubst %.test.yml,%,$$(notdir $$p))))))
endef
######################################################################
TEST_MULTI_SERVER_ALL_TESTS := $(foreach s,$(TEST_MULTI_SERVER_SUITES),$(TEST_MULTI_SERVER_TESTS.$(s)))
+TEST_MULTI_SERVER_PROF_TESTS := $(foreach s,$(filter prof-%,$(TEST_MULTI_SERVER_SUITES)),$(TEST_MULTI_SERVER_TESTS.$(s)))
+
######################################################################
#
# Top-level targets
.PHONY: test.multi-server.ci
test.multi-server.ci: $(TEST_MULTI_SERVER_CI_TESTS)
+#
+# Ensure the freeradius-prof image is present before running
+# any of the profiling tests.
+#
+
+
+# Crossbuild image
+FREERADIUS_CROSSBUILD_IMAGE := freeradius40x-build/ubuntu24:latest
+# Base profiling image, FreeRADIUS not built on this image
+FREERADIUS_PROF_IMAGE := freeradius4-$(PROFILE)/ubuntu24:latest
+# Multi-server profiling image; FreeRADIUS dev build specifically for profiling
+FREERADIUS_RADENV_PROF_IMAGE := freeradius-prof:latest
+
+.PHONY: freeradius-prof.image
+freeradius-prof.image:
+ ${Q}if [ -n "$(FORCE_IMAGE_REBUILD)" ]; then \
+ $(MAKE) -C $(top_srcdir) crossbuild.ubuntu24.profile.regen; \
+ $(MAKE) -C $(top_srcdir) crossbuild.ubuntu24.profile.build; \
+ ./src/tests/multi-server/scripts/docker/build/build_image.sh $(if $(BUILD_PLATFORM),BUILD_PLATFORM=$(BUILD_PLATFORM)); \
+ elif [ -z "$$(docker images -q $(FREERADIUS_PROF_IMAGE) 2>/dev/null)" ]; then \
+ $(MAKE) -C $(top_srcdir) crossbuild.ubuntu24.profile.regen; \
+ $(MAKE) -C $(top_srcdir) crossbuild.ubuntu24.profile.build; \
+ ./src/tests/multi-server/scripts/docker/build/build_image.sh $(if $(BUILD_PLATFORM),BUILD_PLATFORM=$(BUILD_PLATFORM)); \
+ elif [ -z "$$(docker images -q $(FREERADIUS_RADENV_PROF_IMAGE) 2>/dev/null)" ]; then \
+ ./src/tests/multi-server/scripts/docker/build/build_image.sh $(if $(BUILD_PLATFORM),BUILD_PLATFORM=$(BUILD_PLATFORM)); \
+ else \
+ echo "$(FREERADIUS_PROF_IMAGE) and $(FREERADIUS_RADENV_PROF_IMAGE) available, skipping image creation"; \
+ fi
+
+$(TEST_MULTI_SERVER_PROF_TESTS): freeradius-prof.image
+
+#
+# Copy the valgrind profiling helper script into each prof test's output dir
+# so it is available alongside the rendered test configs.
+#
+PROFILING_SCRIPT_SRC := $(DIR)/scripts/profiling/start_valgrind_profiling.sh
+
+define TEST_MULTI_SERVER_PROF_SCRIPT
+$(OUTPUT)/${1}/${2}/start_valgrind_profiling.sh: $(PROFILING_SCRIPT_SRC)
+ $${Q}mkdir -p $$(@D)
+ $${Q}cp $$< $$@
+
+test.multi-server.${1}.${2}: $(OUTPUT)/${1}/${2}/start_valgrind_profiling.sh
+endef
+
+$(foreach s,$(filter prof-%,$(TEST_MULTI_SERVER_SUITES)),$(foreach p,$(TEST_MULTI_SERVER_PARAM_FILES.$(s)),$(eval $(call TEST_MULTI_SERVER_PROF_SCRIPT,$(s),$(subst .,_,$(patsubst %.test.yml,%,$(notdir $(p))))))))
+
+#
+# Ensure the ldap image is present before running prof-ldap tests.
+# Builds it automatically via docker.openldap.prof if not found.
+#
+OPENLDAP_PROF_IMAGE := freeradius4/openldap-prof:latest
+
+.PHONY: openldap.image
+openldap.image:
+ ${Q}if [ -n "$(FORCE_IMAGE_REBUILD)" ] || [ -z "$$(docker images -q $(OPENLDAP_PROF_IMAGE) 2>/dev/null)" ]; then \
+ $(MAKE) -C $(top_srcdir) docker.openldap.prof; \
+ else \
+ echo "$(OPENLDAP_PROF_IMAGE) available, skipping image creation"; \
+ fi
+ ${Q}docker tag $(OPENLDAP_PROF_IMAGE) openldap:latest
+
+$(TEST_MULTI_SERVER_TESTS.prof-ldap): openldap.image
+
.PHONY: clean.test.multi-server
clean.test.multi-server:
${Q}rm -rf $(OUTPUT)
--- /dev/null
+ldap {
+ server = 'openldap'
+ identity = 'cn=admin,dc=example,dc=com'
+ password = 'adminpassword'
+ base_dn = 'dc=example,dc=com'
+
+ update {
+ control.Password.With-Header += 'userPassword'
+ }
+
+ user {
+ base_dn = "${..base_dn}"
+ filter = "(uid=%{Stripped-User-Name || User-Name})"
+ }
+
+ options {
+ res_timeout = 10
+ srv_timelimit = 3
+ net_timeout = 10
+ }
+
+ pool {
+ start = 0
+ min = 1
+ max = 5
+ connecting = 2
+ uses = 0
+ lifetime = 0
+ }
+}
--- /dev/null
+
+pap {
+ password_attribute = User-Password
+}
--- /dev/null
+sql {
+ dialect = "mysql"
+ driver = "${dialect}"
+
+ $-INCLUDE ${modconfdir}/sql/driver/${driver}
+
+ server = "mariadb"
+ port = 3306
+ login = "radius"
+ password = "radpass"
+
+ radius_db = "radius"
+
+ acct_table1 = "radacct"
+ acct_table2 = "radacct"
+ postauth_table = "radpostauth"
+ authcheck_table = "radcheck"
+ groupcheck_table = "radgroupcheck"
+ authreply_table = "radreply"
+ groupreply_table = "radgroupreply"
+ usergroup_table = "radusergroup"
+
+ pool {
+ start = 0
+ min = 1
+ max = 32
+ connecting = 2
+ uses = 0
+ lifetime = 0
+ }
+
+ group_attribute = "${.:instance}-Group"
+
+ $INCLUDE ${modconfdir}/${.:name}/main/${dialect}/queries.conf
+}
--- /dev/null
+User-Name = "testuser"
+User-Password = "testpass"
+Calling-Station-ID = "F1-F2-F3-F4-F5-F6"
--- /dev/null
+testuser Password.Cleartext := "testpass"
--- /dev/null
+export TEST_LOADGEN_START_PPS="100"
+export TEST_LOADGEN_MAX_PPS="100"
+export TEST_LOADGEN_DURATION="60"
+export TEST_LOADGEN_STEP="100"
+export TEST_LOADGEN_PARALLEL="1"
+export TEST_LOADGEN_MAX_BACKLOG="1000"
+export TEST_LOADGEN_REPEAT="no"
+export TEST_LOADGEN_NUM_MESSAGES=0
+
+for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+ TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+done
+
+export TEST_LOADGEN_NUM_MESSAGES
--- /dev/null
+name = profiling-server
+
+raddbdir = /etc/freeradius
+confdir = ${raddbdir}
+modconfdir = ${confdir}/mods-config
+logdir = /var/log/freeradius
+radacctdir = ${logdir}/radacct
+
+security {
+ allow_core_dumps = yes
+}
+
+modules {
+
+ # Common ldap module configuration
+ {% include "freeradius/common/mods-available/ldap" %}
+
+ # Common pap module configuration
+ {% include "freeradius/common/mods-available/pap" %}
+
+ # Common always module configuration, required for the control policy
+ {% include "freeradius/common/mods-available/always" %}
+
+}
+
+policy {
+
+ # Common control policy configuration, needed for accept action
+ {% include "freeradius/common/policy.d/control" %}
+
+}
+
+server profiling-server {
+
+ namespace = radius
+
+ listen load {
+ handler = load
+ type = Access-Request
+ transport = step
+
+ step {
+
+ # Default packet config to use by proto_load module
+ filename = ${confdir}/load-generator-packets/packet.conf
+
+ # Saving proto_load statistics disabled by default, can be enabled for debugging purposes.
+ csv = ${confdir}/stats/load-generator-stats.csv
+
+ max_attributes = 64
+
+ #
+ # The load profile is configured via environment variables set
+ # in the testcase configuration files.
+ #
+ start_pps = $ENV{TEST_LOADGEN_START_PPS}
+ max_pps = $ENV{TEST_LOADGEN_MAX_PPS}
+ duration = $ENV{TEST_LOADGEN_DURATION}
+ step = $ENV{TEST_LOADGEN_STEP}
+ max_backlog = $ENV{TEST_LOADGEN_MAX_BACKLOG}
+ parallel = $ENV{TEST_LOADGEN_PARALLEL}
+ num_messages = $ENV{TEST_LOADGEN_NUM_MESSAGES}
+ repeat = no
+ }
+ }
+
+ listen authentication {
+ type = Access-Request
+ transport = udp
+ require_message_authenticator = auto
+ limit_proxy_state = auto
+
+ limit {
+ max_clients = 256
+ max_connections = 256
+ idle_timeout = 60.0
+ dynamic_timeout = 600.0
+ nak_lifetime = 30.0
+ cleanup_delay = 5.0
+ }
+
+ udp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+
+ tcp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+ }
+
+ listen authentication {
+ type = Access-Request
+ transport = tcp
+
+ tcp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+ }
+
+ client localhost {
+ shortname = client-localhost
+ ipaddr = *
+ secret = testing123
+ }
+
+ recv Access-Request {
+ ldap
+ # pap module always listed last
+ pap
+ }
+
+ authenticate pap {
+ pap
+ }
+
+ send Access-Accept {
+
+ }
+
+ send Access-Reject {
+
+ }
+
+}
--- /dev/null
+"time","last_packet","rtt","rttvar","pps","pps_accepted","sent","received","backlog","max_backlog","<usec","us","10us","100us","ms","10ms","100ms","s","blocked"
--- /dev/null
+#
+# virtual server templates placeholder
+#
--- /dev/null
+User-Name = "testuser"
+User-Password = "testpass"
+Calling-Station-ID = "F1-F2-F3-F4-F5-F6"
--- /dev/null
+testuser Password.Cleartext := "testpass"
--- /dev/null
+export TEST_LOADGEN_START_PPS="100"
+export TEST_LOADGEN_MAX_PPS="100"
+export TEST_LOADGEN_DURATION="60"
+export TEST_LOADGEN_STEP="100"
+export TEST_LOADGEN_PARALLEL="1"
+export TEST_LOADGEN_MAX_BACKLOG="1000"
+export TEST_LOADGEN_REPEAT="no"
+export TEST_LOADGEN_NUM_MESSAGES=0
+
+for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+ TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+done
+
+export TEST_LOADGEN_NUM_MESSAGES
--- /dev/null
+name = profiling-server
+
+raddbdir = /etc/freeradius
+confdir = ${raddbdir}
+modconfdir = ${confdir}/mods-config
+logdir = /var/log/freeradius
+radacctdir = ${logdir}/radacct
+
+security {
+ allow_core_dumps = yes
+}
+
+modules {
+
+ # Common sql module configuration
+ {% include "freeradius/common/mods-available/sql" %}
+
+ # Common pap module configuration
+ {% include "freeradius/common/mods-available/pap" %}
+
+ # Common always module configuration, required for the control policy
+ {% include "freeradius/common/mods-available/always" %}
+
+}
+
+policy {
+
+ # Common control policy configuration, needed for accept action
+ {% include "freeradius/common/policy.d/control" %}
+
+}
+
+server profiling-server {
+
+ namespace = radius
+
+ listen load {
+ handler = load
+ type = Access-Request
+ transport = step
+
+ step {
+
+ # Default packet config to use by proto_load module
+ filename = ${confdir}/load-generator-packets/packet.conf
+
+ # Saving proto_load statistics disabled by default, can be enabled for debugging purposes.
+ csv = ${confdir}/stats/load-generator-stats.csv
+
+ max_attributes = 64
+
+ #
+ # The load profile is configured via environment variables set
+ # in the testcase configuration files.
+ #
+ start_pps = $ENV{TEST_LOADGEN_START_PPS}
+ max_pps = $ENV{TEST_LOADGEN_MAX_PPS}
+ duration = $ENV{TEST_LOADGEN_DURATION}
+ step = $ENV{TEST_LOADGEN_STEP}
+ max_backlog = $ENV{TEST_LOADGEN_MAX_BACKLOG}
+ parallel = $ENV{TEST_LOADGEN_PARALLEL}
+ num_messages = $ENV{TEST_LOADGEN_NUM_MESSAGES}
+ repeat = no
+ }
+ }
+
+ listen authentication {
+ type = Access-Request
+ transport = udp
+ require_message_authenticator = auto
+ limit_proxy_state = auto
+
+ limit {
+ max_clients = 256
+ max_connections = 256
+ idle_timeout = 60.0
+ dynamic_timeout = 600.0
+ nak_lifetime = 30.0
+ cleanup_delay = 5.0
+ }
+
+ udp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+
+ tcp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+ }
+
+ listen authentication {
+ type = Access-Request
+ transport = tcp
+
+ tcp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+ }
+
+ client localhost {
+ shortname = client-localhost
+ ipaddr = *
+ secret = testing123
+ }
+
+ recv Access-Request {
+ sql
+ # pap module always listed last
+ pap
+ }
+
+ authenticate pap {
+ pap
+ }
+
+ send Access-Accept {
+
+ }
+
+ send Access-Reject {
+
+ }
+
+}
--- /dev/null
+"time","last_packet","rtt","rttvar","pps","pps_accepted","sent","received","backlog","max_backlog","<usec","us","10us","100us","ms","10ms","100ms","s","blocked"
--- /dev/null
+#
+# virtual server templates placeholder
+#
--- /dev/null
+User-Name = "testuser"
+User-Password = "testpass"
+Calling-Station-ID = "F1-F2-F3-F4-F5-F6"
--- /dev/null
+testuser Password.Cleartext := "testpass"
--- /dev/null
+export TEST_LOADGEN_START_PPS="100"
+export TEST_LOADGEN_MAX_PPS="100"
+export TEST_LOADGEN_DURATION="60"
+export TEST_LOADGEN_STEP="100"
+export TEST_LOADGEN_PARALLEL="1"
+export TEST_LOADGEN_MAX_BACKLOG="1000"
+export TEST_LOADGEN_REPEAT="no"
+export TEST_LOADGEN_NUM_MESSAGES=0
+
+for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+ TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+done
+
+export TEST_LOADGEN_NUM_MESSAGES
--- /dev/null
+name = profiling-server
+
+raddbdir = /etc/freeradius
+confdir = ${raddbdir}
+modconfdir = ${confdir}/mods-config
+logdir = /var/log/freeradius
+radacctdir = ${logdir}/radacct
+
+security {
+ allow_core_dumps = yes
+}
+
+modules {
+
+ # Common files module configuration
+ {% include "freeradius/common/mods-available/files" %}
+
+ # Common pap module configuration
+ {% include "freeradius/common/mods-available/pap" %}
+
+ # Common always module configuration, required for the control policy
+ {% include "freeradius/common/mods-available/always" %}
+
+}
+
+policy {
+
+ # Common control policy configuration, needed for accept action
+ {% include "freeradius/common/policy.d/control" %}
+
+}
+
+server profiling-server {
+
+ namespace = radius
+
+ listen load {
+ handler = load
+ type = Access-Request
+ transport = step
+
+ step {
+
+ # Default packet config to use by proto_load module
+ filename = ${confdir}/load-generator-packets/packet.conf
+
+ # Saving proto_load statistics disabled by default, can be enabled for debugging purposes.
+ csv = ${confdir}/stats/load-generator-stats.csv
+
+ max_attributes = 64
+
+ #
+ # The load profile is configured via environment variables set
+ # in the testcase configuration files.
+ #
+ start_pps = $ENV{TEST_LOADGEN_START_PPS}
+ max_pps = $ENV{TEST_LOADGEN_MAX_PPS}
+ duration = $ENV{TEST_LOADGEN_DURATION}
+ step = $ENV{TEST_LOADGEN_STEP}
+ max_backlog = $ENV{TEST_LOADGEN_MAX_BACKLOG}
+ parallel = $ENV{TEST_LOADGEN_PARALLEL}
+ num_messages = $ENV{TEST_LOADGEN_NUM_MESSAGES}
+ repeat = no
+ }
+ }
+
+ listen authentication {
+ type = Access-Request
+ transport = udp
+ require_message_authenticator = auto
+ limit_proxy_state = auto
+
+ limit {
+ max_clients = 256
+ max_connections = 256
+ idle_timeout = 60.0
+ dynamic_timeout = 600.0
+ nak_lifetime = 30.0
+ cleanup_delay = 5.0
+ }
+
+ udp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+
+ tcp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+ }
+
+ listen authentication {
+ type = Access-Request
+ transport = tcp
+
+ tcp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+ }
+
+ client localhost {
+ shortname = client-localhost
+ ipaddr = *
+ secret = testing123
+ }
+
+ recv Access-Request {
+ files
+ # pap module always listed last
+ pap
+ }
+
+ authenticate pap {
+ pap
+ }
+
+ send Access-Accept {
+
+ }
+
+ send Access-Reject {
+
+ }
+
+}
--- /dev/null
+"time","last_packet","rtt","rttvar","pps","pps_accepted","sent","received","backlog","max_backlog","<usec","us","10us","100us","ms","10ms","100ms","s","blocked"
--- /dev/null
+#
+# virtual server templates placeholder
+#
--- /dev/null
+User-Name = "testuser"
+User-Password = "testpass"
+Calling-Station-ID = "F1-F2-F3-F4-F5-F6"
--- /dev/null
+testuser Password.Cleartext := "testpass"
--- /dev/null
+export TEST_LOADGEN_START_PPS="100"
+export TEST_LOADGEN_MAX_PPS="100"
+export TEST_LOADGEN_DURATION="60"
+export TEST_LOADGEN_STEP="100"
+export TEST_LOADGEN_PARALLEL="1"
+export TEST_LOADGEN_MAX_BACKLOG="1000"
+export TEST_LOADGEN_REPEAT="no"
+export TEST_LOADGEN_NUM_MESSAGES=0
+
+for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+ TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+done
+
+export TEST_LOADGEN_NUM_MESSAGES
--- /dev/null
+name = profiling-server
+
+raddbdir = /etc/freeradius
+confdir = ${raddbdir}
+modconfdir = ${confdir}/mods-config
+logdir = /var/log/freeradius
+radacctdir = ${logdir}/radacct
+
+security {
+ allow_core_dumps = yes
+}
+
+modules {
+
+ # Common always module configuration, required for the control policy
+ {% include "freeradius/common/mods-available/always" %}
+
+}
+
+policy {
+ # Common control policy configuration, needed for accept action
+ {% include "freeradius/common/policy.d/control" %}
+}
+
+server profiling-server {
+
+ namespace = radius
+
+ listen load {
+ handler = load
+ type = Access-Request
+ transport = step
+
+ step {
+
+ # Default packet config to use by proto_load module
+ filename = ${confdir}/load-generator-packets/packet.conf
+
+ # Saving proto_load statistics disabled by default, can be enabled for debugging purposes.
+ csv = ${confdir}/stats/load-generator-stats.csv
+
+ max_attributes = 64
+
+ #
+ # The load profile is configured via environment variables set
+ # in the testcase configuration files.
+ #
+ start_pps = $ENV{TEST_LOADGEN_START_PPS}
+ max_pps = $ENV{TEST_LOADGEN_MAX_PPS}
+ duration = $ENV{TEST_LOADGEN_DURATION}
+ step = $ENV{TEST_LOADGEN_STEP}
+ max_backlog = $ENV{TEST_LOADGEN_MAX_BACKLOG}
+ parallel = $ENV{TEST_LOADGEN_PARALLEL}
+ num_messages = $ENV{TEST_LOADGEN_NUM_MESSAGES}
+ repeat = no
+ }
+ }
+
+ listen authentication {
+ type = Access-Request
+ transport = udp
+ require_message_authenticator = auto
+ limit_proxy_state = auto
+
+ limit {
+ max_clients = 256
+ max_connections = 256
+ idle_timeout = 60.0
+ dynamic_timeout = 600.0
+ nak_lifetime = 30.0
+ cleanup_delay = 5.0
+ }
+
+ udp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+
+ tcp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+ }
+
+ listen authentication {
+ type = Access-Request
+ transport = tcp
+
+ tcp {
+ ipaddr = *
+ port = 1812
+ networks {
+ allow = 127/8
+ allow = 192.0.2/24
+ }
+ }
+ }
+
+ client localhost {
+ shortname = client-localhost
+ ipaddr = *
+ secret = testing123
+ }
+
+ recv Access-Request {
+ accept
+ }
+
+ send Access-Accept {
+
+ }
+
+ send Access-Reject {
+
+ }
+
+}
--- /dev/null
+"time","last_packet","rtt","rttvar","pps","pps_accepted","sent","received","backlog","max_backlog","<usec","us","10us","100us","ms","10ms","100ms","s","blocked"
--- /dev/null
+#
+# virtual server templates placeholder
+#
--- /dev/null
+CREATE TABLE radcheck (
+ id INT(11) NOT NULL AUTO_INCREMENT,
+ username VARCHAR(64) NOT NULL DEFAULT '',
+ attribute VARCHAR(64) NOT NULL DEFAULT '',
+ op CHAR(2) NOT NULL DEFAULT '==',
+ value VARCHAR(253) NOT NULL DEFAULT '',
+ PRIMARY KEY (id),
+ KEY username (username(32))
+);
+
+INSERT INTO radcheck (username, attribute, op, value)
+VALUES ('testuser', 'Password.Cleartext', ':=', 'testpass');
+
+CREATE TABLE radreply (
+ id INT(11) NOT NULL AUTO_INCREMENT,
+ username VARCHAR(64) NOT NULL DEFAULT '',
+ attribute VARCHAR(64) NOT NULL DEFAULT '',
+ op CHAR(2) NOT NULL DEFAULT '==',
+ value VARCHAR(253) NOT NULL DEFAULT '',
+ PRIMARY KEY (id),
+ KEY username (username(32))
+);
+
+INSERT INTO radreply (username, attribute, op, value)
+VALUES ('testuser', 'Reply-Message', ':=', 'Hello, testuser!');
+
+
+# Set up the other tables similarly
+CREATE TABLE radacct (
+ radacctid bigint(21) NOT NULL auto_increment,
+ acctsessionid varchar(64) NOT NULL default '',
+ acctuniqueid varchar(32) NOT NULL default '',
+ username varchar(64) NOT NULL default '',
+ groupname varchar(64) NOT NULL default '',
+ realm varchar(64) default '',
+ nasipaddress varchar(15) NOT NULL default '',
+ nasportid varchar(32) default NULL,
+ nasporttype varchar(32) default NULL,
+ acctstarttime datetime NULL default NULL,
+ acctupdatetime datetime NULL default NULL,
+ acctstoptime datetime NULL default NULL,
+ acctinterval int(12) default NULL,
+ acctsessiontime int(12) unsigned default NULL,
+ acctauthentic varchar(32) default NULL,
+ connectinfo_start varchar(50) default NULL,
+ connectinfo_stop varchar(50) default NULL,
+ acctinputoctets bigint(20) default NULL,
+ acctoutputoctets bigint(20) default NULL,
+ calledstationid varchar(50) NOT NULL default '',
+ callingstationid varchar(50) NOT NULL default '',
+ acctterminatecause varchar(32) NOT NULL default '',
+ servicetype varchar(32) default NULL,
+ framedprotocol varchar(32) default NULL,
+ framedipaddress varchar(15) NOT NULL default '',
+ framedipv6address varchar(45) NOT NULL default '',
+ framedipv6prefix varchar(45) NOT NULL default '',
+ framedinterfaceid varchar(44) NOT NULL default '',
+ delegatedipv6prefix varchar(45) NOT NULL default '',
+ class varchar(64) default NULL,
+ PRIMARY KEY (radacctid),
+ UNIQUE KEY acctuniqueid (acctuniqueid),
+ KEY username (username),
+ KEY framedipaddress (framedipaddress),
+ KEY framedipv6address (framedipv6address),
+ KEY framedipv6prefix (framedipv6prefix),
+ KEY framedinterfaceid (framedinterfaceid),
+ KEY delegatedipv6prefix (delegatedipv6prefix),
+ KEY acctsessionid (acctsessionid),
+ KEY acctsessiontime (acctsessiontime),
+ KEY acctstarttime (acctstarttime),
+ KEY acctinterval (acctinterval),
+ KEY acctstoptime (acctstoptime),
+ KEY nasipaddress (nasipaddress),
+ INDEX bulk_close (acctstoptime, nasipaddress, acctstarttime)
+) ENGINE = INNODB;
+
+CREATE TABLE radgroupcheck (
+ id int(11) unsigned NOT NULL auto_increment,
+ groupname varchar(64) NOT NULL default '',
+ attribute varchar(64) NOT NULL default '',
+ op char(2) NOT NULL DEFAULT '==',
+ value varchar(253) NOT NULL default '',
+ PRIMARY KEY (id),
+ KEY groupname (groupname(32))
+);
+
+CREATE TABLE radgroupreply (
+ id int(11) unsigned NOT NULL auto_increment,
+ groupname varchar(64) NOT NULL default '',
+ attribute varchar(64) NOT NULL default '',
+ op char(2) NOT NULL DEFAULT '=',
+ value varchar(253) NOT NULL default '',
+ PRIMARY KEY (id),
+ KEY groupname (groupname(32))
+);
+
+CREATE TABLE radusergroup (
+ id int(11) unsigned NOT NULL auto_increment,
+ username varchar(64) NOT NULL default '',
+ groupname varchar(64) NOT NULL default '',
+ priority int(11) NOT NULL default '1',
+ PRIMARY KEY (id),
+ KEY username (username(32))
+);
+
+CREATE TABLE radpostauth (
+ id int(11) NOT NULL auto_increment,
+ username varchar(64) NOT NULL default '',
+ pass varchar(64) NOT NULL default '',
+ reply varchar(32) NOT NULL default '',
+ authdate timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+ class varchar(64) NOT NULL default '',
+ PRIMARY KEY (id)
+) ENGINE = INNODB;
+
+CREATE TABLE nas (
+ id int(10) NOT NULL auto_increment,
+ nasname varchar(128) NOT NULL,
+ shortname varchar(32),
+ type varchar(30) DEFAULT 'other',
+ ports int(5),
+ secret varchar(60) DEFAULT 'secret' NOT NULL,
+ server varchar(64),
+ community varchar(50),
+ description varchar(200) DEFAULT 'RADIUS Client',
+ require_ma varchar(4) DEFAULT 'auto',
+ limit_proxy_state varchar(4) DEFAULT 'auto',
+ PRIMARY KEY (id),
+ KEY nasname (nasname)
+) ENGINE = INNODB;
+
+CREATE TABLE IF NOT EXISTS nasreload (
+ nasipaddress varchar(15) NOT NULL,
+ reloadtime datetime NOT NULL,
+ PRIMARY KEY (nasipaddress)
+) ENGINE = INNODB;
--- /dev/null
+# ---------------------------------------------------------------
+# Docker Compose Test Environment:
+#
+# A single server instance for profiling
+#
+# ---------------------------------------------------------------
+x-common-config: &id001
+ cap_add:
+ - NET_ADMIN
+ - SYS_PTRACE
+ environment:
+ TEST_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
+ TEST_SUBNET: {{ test_subnet | default('172.16.0.0/12') }}
+services:
+ openldap:
+ image: openldap:latest
+ healthcheck:
+ test: ["CMD-SHELL", "ldapsearch -x -H ldap://localhost -D 'cn=admin,dc=example,dc=com' -w adminpassword -b 'dc=example,dc=com' > /dev/null 2>&1"]
+ interval: 2s
+ timeout: 5s
+ retries: 10
+ start_period: 15s
+ <<: *id001
+ profiling-server:
+ image: freeradius-prof:latest
+ depends_on:
+ openldap:
+ condition: service_healthy
+ volumes:
+ # profiling-server server config
+ - ${DATA_PATH}/freeradius/profiling-ldap/radiusd.conf:/etc/freeradius/radiusd.conf
+ # files module configuration
+ - ${DATA_PATH}/freeradius/profiling-ldap/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize
+ # proto_load packet configuration and statistics output
+ - ${DATA_PATH}/freeradius/profiling-ldap/proto_load_config.env:/etc/freeradius/proto_load_config.env
+ - ${DATA_PATH}/freeradius/profiling-ldap/load-generator-packets/:/etc/freeradius/load-generator-packets/
+ - ${DATA_PATH}/freeradius/profiling-ldap/stats/load-generator-stats.csv:/etc/freeradius/stats/load-generator-stats.csv
+ # Profiling scripts
+ - ${DATA_PATH}/start_valgrind_profiling.sh:/etc/freeradius/start_valgrind_profiling.sh
+ # Profiling results directory
+ - ${PROF_RESULTS_PATH}/:/etc/prof-results/
+ # Listener directory
+ - ${LISTENER_DIR}/:/var/run/multi-server/
+ entrypoint:
+ - bash
+ - -lc
+ - |
+ # Keep the container alive. The test framework starts FreeRADIUS
+ # and runs commands via 'docker exec' so it can control timing.
+ #
+ # Start the server after configuring environment variables from test case's template.yml.j2 file.
+ sleep infinity
+ <<: *id001
+
--- /dev/null
+# ---------------------------------------------------------------
+# Docker Compose Test Environment:
+#
+# A single server instance for profiling
+#
+# ---------------------------------------------------------------
+x-common-config: &id001
+ cap_add:
+ - NET_ADMIN
+ - SYS_PTRACE
+ environment:
+ TEST_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
+ TEST_SUBNET: {{ test_subnet | default('172.16.0.0/12') }}
+services:
+ mariadb:
+ image: mariadb:10.5
+ environment:
+ MYSQL_ROOT_PASSWORD: rootpass
+ MYSQL_DATABASE: radius
+ MYSQL_USER: radius
+ MYSQL_PASSWORD: radpass
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p${MYSQL_ROOT_PASSWORD} > /dev/null 2>&1"]
+ interval: 2s
+ timeout: 5s
+ retries: 10
+ start_period: 15s
+ volumes:
+ - /tmp/${COMPOSE_PROJECT_NAME}-mariadb-data:/var/lib/mysql
+ - ${DATA_PATH}/mariadb/default/init.sql:/docker-entrypoint-initdb.d/init.sql
+ <<: *id001
+ profiling-server:
+ image: freeradius-prof:latest
+ depends_on:
+ mariadb:
+ condition: service_healthy
+ volumes:
+ # profiling-server server config
+ - ${DATA_PATH}/freeradius/profiling-mysql/radiusd.conf:/etc/freeradius/radiusd.conf
+ # files module configuration
+ - ${DATA_PATH}/freeradius/profiling-mysql/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize
+ # proto_load packet configuration and statistics output
+ - ${DATA_PATH}/freeradius/profiling-mysql/proto_load_config.env:/etc/freeradius/proto_load_config.env
+ - ${DATA_PATH}/freeradius/profiling-mysql/load-generator-packets/:/etc/freeradius/load-generator-packets/
+ - ${DATA_PATH}/freeradius/profiling-mysql/stats/load-generator-stats.csv:/etc/freeradius/stats/load-generator-stats.csv
+ # Profiling scripts
+ - ${DATA_PATH}/start_valgrind_profiling.sh:/etc/freeradius/start_valgrind_profiling.sh
+ # Profiling results directory
+ - ${PROF_RESULTS_PATH}/:/etc/prof-results/
+ # Listener directory
+ - ${LISTENER_DIR}/:/var/run/multi-server/
+ entrypoint:
+ - bash
+ - -lc
+ - |
+ # Keep the container alive. The test framework starts FreeRADIUS
+ # and runs commands via 'docker exec' so it can control timing.
+ #
+ # Start the server after configuring environment variables from test case's template.yml.j2 file.
+ sleep infinity
+ <<: *id001
+
--- /dev/null
+# ---------------------------------------------------------------
+# Docker Compose Test Environment:
+#
+# A single server instance for profiling
+#
+# ---------------------------------------------------------------
+x-common-config: &id001
+ cap_add:
+ - NET_ADMIN
+ - SYS_PTRACE
+ environment:
+ TEST_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
+ TEST_SUBNET: {{ test_subnet | default('172.16.0.0/12') }}
+services:
+ profiling-server:
+ image: freeradius-prof:latest
+ volumes:
+ # profiling-server server config
+ - ${DATA_PATH}/freeradius/profiling-server-pap-auth/radiusd.conf:/etc/freeradius/radiusd.conf
+ # files module configuration
+ - ${DATA_PATH}/freeradius/profiling-server-pap-auth/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize
+ # proto_load packet configuration and statistics output
+ - ${DATA_PATH}/freeradius/profiling-server-pap-auth/proto_load_config.env:/etc/freeradius/proto_load_config.env
+ - ${DATA_PATH}/freeradius/profiling-server-pap-auth/load-generator-packets/:/etc/freeradius/load-generator-packets/
+ - ${DATA_PATH}/freeradius/profiling-server-pap-auth/stats/load-generator-stats.csv:/etc/freeradius/stats/load-generator-stats.csv
+ # Profiling scripts
+ - ${DATA_PATH}/start_valgrind_profiling.sh:/etc/freeradius/start_valgrind_profiling.sh
+ # Profiling results directory
+ - ${PROF_RESULTS_PATH}/:/etc/prof-results/
+ # Listener directory
+ - ${LISTENER_DIR}/:/var/run/multi-server/
+ entrypoint:
+ - bash
+ - -lc
+ - |
+ # Keep the container alive. The test framework starts FreeRADIUS
+ # and runs commands via 'docker exec' so it can control timing.
+ #
+ # Start the server after configuring environment variables from test case's template.yml.j2 file.
+ sleep infinity
+ <<: *id001
--- /dev/null
+# ---------------------------------------------------------------
+# Docker Compose Test Environment:
+#
+# A single server instance for profiling
+#
+# ---------------------------------------------------------------
+x-common-config: &id001
+ cap_add:
+ - NET_ADMIN
+ - SYS_PTRACE
+ environment:
+ TEST_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
+ TEST_SUBNET: {{ test_subnet | default('172.16.0.0/12') }}
+services:
+ profiling-server:
+ image: freeradius-prof:latest
+ volumes:
+ # profiling-server server config
+ - ${DATA_PATH}/freeradius/profiling-server/radiusd.conf:/etc/freeradius/radiusd.conf
+ # proto_load packet configuration and statistics output
+ - ${DATA_PATH}/freeradius/profiling-server/proto_load_config.env:/etc/freeradius/proto_load_config.env
+ - ${DATA_PATH}/freeradius/profiling-server/load-generator-packets/:/etc/freeradius/load-generator-packets/
+ - ${DATA_PATH}/freeradius/profiling-server/stats/load-generator-stats.csv:/etc/freeradius/stats/load-generator-stats.csv
+ # Profiling scripts
+ - ${DATA_PATH}/start_valgrind_profiling.sh:/etc/freeradius/start_valgrind_profiling.sh
+ # Profiling results directory
+ - ${PROF_RESULTS_PATH}/:/etc/prof-results/
+ # Listener directory
+ - ${LISTENER_DIR}/:/var/run/multi-server/
+ entrypoint:
+ - bash
+ - -lc
+ - |
+ # Keep the container alive. The test framework starts FreeRADIUS
+ # and runs commands via 'docker exec' so it can control timing.
+ #
+ # Start the server after configuring environment variables from test case's template.yml.j2 file.
+ sleep infinity
+ <<: *id001
--- /dev/null
+# Dockerfile for multi-server profiling tests.
+# Builds off of "base" profiling image, configures FreeRADIUS and builds it.
+FROM freeradius4-default-profiling/ubuntu24:latest
+
+# CFLAGS used for profiling build:
+# -g3 Maximum debug info for callgrind symbol resolution
+# -O1 Basic optimisation for realistic hotspot costs; inlining/vectorisation/
+# unrolling disabled below so the call graph matches the source.
+# -fno-omit-frame-pointer Keep frame pointers for callgrind stack walking
+# -fno-inline Preserve call edges (suppresses CC_HINT(flatten) too)
+# -Dalways_inline= Strip always_inline, which -fno-inline does not suppress
+# -fno-plt Resolve cross-library calls via GOT rather than PLT stubs;
+# PLT stubs have no DWARF info and show as ??? in callgrind.
+# -fno-builtin Keep stdlib calls (memcpy, strlen, etc.) visible in the graph
+# -fno-optimize-sibling-calls Suppress tail-call elimination (-O1 can still apply it)
+RUN ./configure \
+ --enable-developer \
+ --disable-verify-ptr \
+ --with-raddbdir=/etc/freeradius \
+ CFLAGS="-g3 -O1 -fno-omit-frame-pointer -fno-inline -Dalways_inline= -fno-plt -fno-builtin -fno-optimize-sibling-calls" \
+ LDFLAGS="-fno-omit-frame-pointer"
+
+RUN make
+
+# FreeRADIUS installed in /etc/freeradius
+RUN make install
+
+# Setup softlinks to be able to run server with `freeradius` cmd
+RUN ln -sf /usr/local/sbin/radiusd /usr/local/sbin/freeradius && \
+ ln -sf /etc/freeradius/radiusd.conf /etc/freeradius/freeradius.conf && \
+ ln -sf /etc/freeradius /etc/raddb
+
+# Generate the self-signed RSA (and DH/EC) certificates for testing.
+RUN cd /etc/freeradius/certs && make
+
+
--- /dev/null
+#!/bin/bash
+for arg in "$@"; do
+ case $arg in
+ BUILD_PLATFORM=*) BUILD_PLATFORM="${arg#*=}" ;;
+ esac
+done
+
+# This allows us to build an image on Apple Silicon where the base image was built on an linux/amd64 platform.
+# Example usage: BUILD_PLATFORM=linux/amd64 ./build_image.sh
+PLATFORM_ARG=""
+if [ -n "${BUILD_PLATFORM}" ]; then
+ PLATFORM_ARG="--platform=${BUILD_PLATFORM}"
+fi
+
+docker build ${PLATFORM_ARG} -f src/tests/multi-server/scripts/docker/build/Dockerfile.multi-server-prof -t freeradius-prof:latest .
--- /dev/null
+#!/bin/bash
+for arg in "$@"; do
+ case $arg in
+ BUILD_PLATFORM=*) BUILD_PLATFORM="${arg#*=}" ;;
+ esac
+done
+
+# This allows us to run a container on Apple Silicon where the base image was built on an linux/amd64 platform.
+# Example usage: BUILD_PLATFORM=linux/amd64 ./run_container.sh
+PLATFORM_ARG=""
+if [ -n "${BUILD_PLATFORM}" ]; then
+ PLATFORM_ARG="--platform=${BUILD_PLATFORM}"
+fi
+
+docker run -it --rm ${PLATFORM_ARG} -v "$(pwd)/prof-results:/etc/prof-results" --name freeradius-radenv-container freeradius-prof:latest
--- /dev/null
+## generate_callgrind_report.py
+
+python3 src/tests/multi-server/scripts/generate_callgrind_report.py \
+ <results_dir> \
+ --title "FreeRADIUS prof-accept 5min" \
+ --text-output valgrind_report_radenv_prof_accept.txt \
+ --md-output valgrind_report_radenv_prof_accept.md
+
+## Generate text based report from Valgrind/Callgrind results
+callgrind_annotate $(find . -name "callgrind.out.*" -size +0c | sort) > callgrind_report.txt
+
+## Generate SVG sharable file of valgrind/callgrind results
+
+Dependency: ```brew install gprof2dot```
+
+Generate SVG file for one worker thread:
+```
+gprof2dot --format=callgrind \
+ <path-to-prof-results>/callgrind.out.1004-04 \
+ | dot -Tsvg -o callgraph_thread04.svg
+```
+
+Generate SVG file per worker thread:
+```
+for f in <path-to-prof-results>/callgrind.out.1004-{04..12}; do
+ thread=$(grep "^thread:" "$f" | awk '{print $2}')
+ gprof2dot --format=callgrind "$f" \
+ | dot -Tsvg -o "callgraph_thread${thread}.svg"
+done
+```
--- /dev/null
+#!/usr/bin/env python3
+"""Generate text and markdown profiling reports from callgrind output files."""
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+from collections import defaultdict
+
+
+def parse_thread_number(callgrind_file):
+ with open(callgrind_file) as f:
+ for line in f:
+ if line.startswith('thread:'):
+ return int(line.split()[1])
+ return 0
+
+
+def parse_summary_ir(callgrind_file):
+ with open(callgrind_file) as f:
+ for line in f:
+ if line.startswith('summary:'):
+ return int(line.split()[1])
+ return 0
+
+
+def run_callgrind_annotate(callgrind_file):
+ result = subprocess.run(
+ ['callgrind_annotate', '--auto=no', '--threshold=100', callgrind_file],
+ capture_output=True, text=True
+ )
+ return result.stdout
+
+
+def parse_module_entries(annotate_output):
+ """Extract rlm_* and proto_load function rows from callgrind_annotate output."""
+ entries = []
+ for line in annotate_output.splitlines():
+ if 'rlm_' not in line and 'proto_load' not in line:
+ continue
+ if not line.strip() or line.startswith('-') or line.startswith('='):
+ continue
+
+ ir_match = re.match(r'^\s*([\d,]+)\s+\(\s*([\d.]+)%\)', line)
+ if not ir_match:
+ continue
+
+ func_match = re.search(r'([^/\s]+):(\w+)\s+\[([^\]]+)\]', line)
+ if not func_match:
+ continue
+
+ entries.append({
+ 'ir': int(ir_match.group(1).replace(',', '')),
+ 'ir_pct': float(ir_match.group(2)),
+ 'function': func_match.group(2),
+ 'lib': os.path.basename(func_match.group(3)),
+ })
+ return entries
+
+
+def fmt_ir(n):
+ return f"{n:,}"
+
+
+def generate_markdown(results_dir, thread_data, title):
+ lines = []
+ lines.append(f"# {title}")
+ lines.append("")
+ lines.append(f"**Results:** `{results_dir}`")
+ lines.append("")
+
+ # Collect all unique function+lib pairs
+ lib_to_funcs = defaultdict(set)
+ for td in thread_data.values():
+ for e in td['entries']:
+ lib_to_funcs[e['lib']].add(e['function'])
+
+ lines.append("## Functions Found")
+ lines.append("")
+ lines.append("| Function | Library |")
+ lines.append("|---|---|")
+ for lib in sorted(lib_to_funcs):
+ funcs = ', '.join(f'`{f}`' for f in sorted(lib_to_funcs[lib]))
+ lines.append(f"| {funcs} | `{lib}` |")
+ lines.append("")
+
+ lines.append("## CPU Share (Ir = Instructions Retired)")
+ lines.append("")
+
+ for thread_num, td in sorted(thread_data.items()):
+ if not td['entries']:
+ continue
+
+ total_ir = td['total_ir']
+ lines.append(f"### Thread {thread_num:02d} — Total: {fmt_ir(total_ir)} Ir")
+ lines.append("")
+ lines.append("| Function | Library | Ir | % of Thread |")
+ lines.append("|---|---|---|---|")
+
+ module_total = 0
+ for e in sorted(td['entries'], key=lambda x: -x['ir']):
+ lines.append(f"| `{e['function']}` | `{e['lib']}` | {fmt_ir(e['ir'])} | {e['ir_pct']:.2f}% |")
+ module_total += e['ir']
+
+ module_pct = (module_total / total_ir * 100) if total_ir else 0
+ lines.append(f"| **Total** | | **{fmt_ir(module_total)}** | **{module_pct:.2f}%** |")
+ lines.append("")
+
+ all_module_ir = sum(e['ir'] for td in thread_data.values() for e in td['entries'])
+ all_total_ir = sum(td['total_ir'] for td in thread_data.values())
+ overall_pct = (all_module_ir / all_total_ir * 100) if all_total_ir else 0
+
+ lines.append("## Takeaway")
+ lines.append("")
+ lines.append(
+ f"`rlm_*` and `proto_load` combined account for **{overall_pct:.2f}% of total instructions** "
+ f"across all threads ({fmt_ir(all_module_ir)} of {fmt_ir(all_total_ir)} Ir total)."
+ )
+ lines.append("")
+
+ return "\n".join(lines)
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Generate profiling reports from callgrind results")
+ parser.add_argument("results_dir", help="Directory containing callgrind.out.* files")
+ parser.add_argument("--title", default=None, help="Report title")
+ parser.add_argument("--text-output", default=None, help="Path for combined callgrind_annotate text report")
+ parser.add_argument("--md-output", default=None, help="Path for markdown summary report")
+ args = parser.parse_args()
+
+ results_dir = args.results_dir
+ if not os.path.isdir(results_dir):
+ print(f"error: {results_dir} is not a directory", file=sys.stderr)
+ sys.exit(1)
+
+ files = sorted([
+ os.path.join(results_dir, f)
+ for f in os.listdir(results_dir)
+ if re.match(r'callgrind\.out\.\d+(-\d+)?$', f) and os.path.getsize(os.path.join(results_dir, f)) > 0
+ ])
+
+ if not files:
+ print(f"error: no callgrind.out.* files found in {results_dir}", file=sys.stderr)
+ sys.exit(1)
+
+ title = args.title or f"FreeRADIUS Callgrind Profile: {os.path.basename(os.path.normpath(results_dir))}"
+
+ thread_data = {}
+ text_sections = []
+
+ for f in files:
+ thread_num = parse_thread_number(f)
+ total_ir = parse_summary_ir(f)
+ print(f" {os.path.basename(f)}: thread {thread_num:02d}, {total_ir:,} Ir", file=sys.stderr)
+
+ annotate_output = run_callgrind_annotate(f)
+ text_sections.append(f"{'='*80}\n{os.path.basename(f)} (thread {thread_num:02d})\n{'='*80}\n{annotate_output}")
+
+ if thread_num not in thread_data:
+ thread_data[thread_num] = {'total_ir': 0, 'entries': []}
+ thread_data[thread_num]['total_ir'] += total_ir
+ thread_data[thread_num]['entries'].extend(parse_module_entries(annotate_output))
+
+ if args.text_output:
+ with open(args.text_output, 'w') as out:
+ out.write("\n\n".join(text_sections))
+ print(f"text report -> {args.text_output}", file=sys.stderr)
+
+ md = generate_markdown(results_dir, thread_data, title)
+
+ if args.md_output:
+ with open(args.md_output, 'w') as out:
+ out.write(md)
+ print(f"markdown report -> {args.md_output}", file=sys.stderr)
+ else:
+ print(md)
+
+
+if __name__ == '__main__':
+ main()
--- /dev/null
+#!/bin/bash
+
+# To be run inside the profiling container
+
+# Clear any stale marker from a previous run
+rm -f /etc/prof-results/.profiling_complete
+rm -f /etc/prof-results/valgrind_profiling.log
+
+exec > /etc/prof-results/valgrind_profiling.log 2>&1
+
+# Ignore SIGTERM — freeradius broadcasts it to the process group on shutdown,
+# which would otherwise kill this script before it can touch .profiling_complete
+trap '' SIGTERM
+
+# Load proto_load config to get packet settings
+source /etc/freeradius/proto_load_config.env
+
+# Calculate approximate send duration
+SEND_DURATION=$(( TEST_LOADGEN_NUM_MESSAGES / TEST_LOADGEN_START_PPS ))
+PROFILE_DURATION_BUFFER=15
+PROFILE_DURATION=$SEND_DURATION-$PROFILE_DURATION_BUFFER
+
+# Start freeradius under valgrind with instrumentation off
+valgrind \
+ --tool=callgrind \
+ --callgrind-out-file=/etc/prof-results/callgrind.out.%p \
+ --trace-children=yes \
+ --separate-threads=yes \
+ --dump-instr=yes \
+ --collect-jumps=yes \
+ --cache-sim=yes \
+ --branch-sim=yes \
+ --keep-debuginfo=yes \
+ --instr-atstart=no \
+ freeradius -f -l stdout -S resources.talloc_skip_cleanup=yes 2>&1 | \
+ tee /etc/prof-results/freeradius.log &
+VALGRIND_PID=$!
+
+# Wait for server ready (bail out if freeradius fails to start under valgrind)
+STARTUP_TIMEOUT=300
+STARTUP_ELAPSED=0
+until grep -q "Ready to process requests" /etc/prof-results/freeradius.log; do
+ sleep 1
+ STARTUP_ELAPSED=$(( STARTUP_ELAPSED + 1 ))
+ if [ ${STARTUP_ELAPSED} -ge ${STARTUP_TIMEOUT} ]; then
+ echo "ERROR: freeradius did not become ready within ${STARTUP_TIMEOUT}s, aborting"
+ kill -SIGKILL ${VALGRIND_PID} 2>/dev/null
+ exit 1
+ fi
+done
+
+# Enable instrumentation. callgrind_control auto-detects the running callgrind
+# instance and prints "PID <n>: freeradius ..." — capture that to get the PID
+# we need later for the graceful shutdown signal.
+echo "INFO: enabling callgrind instrumentation"
+CTRL_OUT=$(callgrind_control --instr=on)
+printf '%s\n' "$CTRL_OUT"
+FR_PID=$(printf '%s\n' "$CTRL_OUT" | grep -oP 'PID \K\d+(?=: freeradius)' | head -1)
+echo "Freeradius PID: ${FR_PID}"
+
+# Wait for approximate send duration
+sleep ${SEND_DURATION}
+
+# Stop instrumentation before shutdown so valgrind only flushes already-collected data
+echo "INFO: disabling callgrind instrumentation"
+CTRL_OUT=$(callgrind_control --instr=off 2>/dev/null || true)
+printf '%s\n' "$CTRL_OUT"
+
+# Graceful shutdown (equivalent to Ctrl+C)
+if [ -z "${FR_PID}" ]; then
+ echo "WARNING: could not determine freeradius PID from callgrind_control output, sending SIGINT to valgrind pipeline instead"
+ kill -SIGINT ${VALGRIND_PID} 2>/dev/null || true
+else
+ echo "INFO: killing freeradius process ${FR_PID} with SIGINT for graceful shutdown"
+ kill -SIGINT ${FR_PID}
+fi
+
+# Give valgrind time to write callgrind output after freeradius exits
+echo "INFO: sleeping for 5s"
+sleep 5
+
+# Signal that valgrind has finished writing all profiling data
+echo "INFO: Profiling complete at $(date)"
+
+echo "INFO: running callgrind_annotate to generate report"
+#callgrind_annotate $(find /etc/prof-results -name "callgrind.out.*" -size +0c | sort) > /etc/prof-results/callgrind_report.txt
+cmd='callgrind_annotate $(find /etc/prof-results -name "callgrind.out.*" -size +0c | sort) > /etc/prof-results/callgrind_report.txt'
+echo "$cmd"
+eval "$cmd"
+
+# Restore stdout/stderr
+exec > /dev/null 2>&1
--- /dev/null
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+ start_pps: 500
+ max_pps: 500
+ duration: 300
+ step: 500
+ parallel: 1
+ max_backlog: 1000
+ repeat: "no"
+
+# Test framework
+test_timeout: 350
+test_verify_timeout: 340
--- /dev/null
+../../environments/profiling.yml.j2
\ No newline at end of file
--- /dev/null
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+ start_pps: 100
+ max_pps: 100
+ duration: 60
+ step: 100
+ parallel: 1
+ max_backlog: 1000
+ repeat: "no"
+
+# Test framework
+test_timeout: 125
+test_state1_verify_timeout: 120
+test_state2_verify_timeout: 30
--- /dev/null
+timeout: {{ test_timeout }}
+state_order: sequence
+states:
+ state_1:
+ description: >
+ Baseline profiling test
+ host:
+ profiling-server:
+ actions:
+ - execute_command:
+ command: |
+ #
+ # proto_load configuration via environment variables
+ #
+ {%- for key, value in loadgen.items() %}
+ export TEST_LOADGEN_{{ key | upper }}="{{ value }}"
+ {%- endfor %}
+ TEST_LOADGEN_NUM_MESSAGES=0
+ for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+ TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+ done
+ export TEST_LOADGEN_NUM_MESSAGES
+ #
+ # Starting load-generator server which will generate traffic based on env configuration
+ # from above.
+ #
+ printf "Starting load-generator with the following configuration:\n"
+ {%- for key, value in loadgen.items() %}
+ printf " {{ key | upper }}:%s\n" "$TEST_LOADGEN_{{ key | upper }}"
+ {%- endfor %}
+ printf " NUM_MESSAGES: %s\n" "$TEST_LOADGEN_NUM_MESSAGES"
+
+ source /etc/freeradius/start_valgrind_profiling.sh
+
+ detach: true
+ verify:
+ timeout: {{ test_state1_verify_timeout }}
+ trigger_mode: unordered
--- /dev/null
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+ start_pps: 500
+ max_pps: 500
+ duration: 300
+ step: 500
+ parallel: 1
+ max_backlog: 1000
+ repeat: "no"
+
+# Test framework
+test_timeout: 350
+test_verify_timeout: 340
--- /dev/null
+../../environments/profiling-ldap.yml.j2
\ No newline at end of file
--- /dev/null
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+ start_pps: 100
+ max_pps: 100
+ duration: 60
+ step: 100
+ parallel: 1
+ max_backlog: 1000
+ repeat: "no"
+
+# Test framework
+test_timeout: 125
+test_state1_verify_timeout: 120
+test_state2_verify_timeout: 30
--- /dev/null
+timeout: {{ test_timeout }}
+state_order: sequence
+states:
+ state_1:
+ description: >
+ Baseline profiling test
+ host:
+ profiling-server:
+ actions:
+ - execute_command:
+ command: |
+ #
+ # proto_load configuration via environment variables
+ #
+ {%- for key, value in loadgen.items() %}
+ export TEST_LOADGEN_{{ key | upper }}="{{ value }}"
+ {%- endfor %}
+ TEST_LOADGEN_NUM_MESSAGES=0
+ for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+ TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+ done
+ export TEST_LOADGEN_NUM_MESSAGES
+ #
+ # Starting load-generator server which will generate traffic based on env configuration
+ # from above.
+ #
+ printf "Starting load-generator with the following configuration:\n"
+ {%- for key, value in loadgen.items() %}
+ printf " {{ key | upper }}:%s\n" "$TEST_LOADGEN_{{ key | upper }}"
+ {%- endfor %}
+ printf " NUM_MESSAGES: %s\n" "$TEST_LOADGEN_NUM_MESSAGES"
+
+ source /etc/freeradius/start_valgrind_profiling.sh
+
+ detach: true
+ verify:
+ timeout: {{ test_state1_verify_timeout }}
+ trigger_mode: unordered
--- /dev/null
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+ start_pps: 500
+ max_pps: 500
+ duration: 300
+ step: 500
+ parallel: 1
+ max_backlog: 1000
+ repeat: "no"
+
+# Test framework
+test_timeout: 350
+test_verify_timeout: 340
--- /dev/null
+../../environments/profiling-mysql.yml.j2
\ No newline at end of file
--- /dev/null
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+ start_pps: 100
+ max_pps: 100
+ duration: 60
+ step: 100
+ parallel: 1
+ max_backlog: 1000
+ repeat: "no"
+
+# Test framework
+test_timeout: 125
+test_state1_verify_timeout: 120
+test_state2_verify_timeout: 30
--- /dev/null
+timeout: {{ test_timeout }}
+state_order: sequence
+states:
+ state_1:
+ description: >
+ Baseline profiling test
+ host:
+ profiling-server:
+ actions:
+ - execute_command:
+ command: |
+ #
+ # proto_load configuration via environment variables
+ #
+ {%- for key, value in loadgen.items() %}
+ export TEST_LOADGEN_{{ key | upper }}="{{ value }}"
+ {%- endfor %}
+ TEST_LOADGEN_NUM_MESSAGES=0
+ for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+ TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+ done
+ export TEST_LOADGEN_NUM_MESSAGES
+ #
+ # Starting load-generator server which will generate traffic based on env configuration
+ # from above.
+ #
+ printf "Starting load-generator with the following configuration:\n"
+ {%- for key, value in loadgen.items() %}
+ printf " {{ key | upper }}:%s\n" "$TEST_LOADGEN_{{ key | upper }}"
+ {%- endfor %}
+ printf " NUM_MESSAGES: %s\n" "$TEST_LOADGEN_NUM_MESSAGES"
+
+ source /etc/freeradius/start_valgrind_profiling.sh
+
+ detach: true
+ verify:
+ timeout: {{ test_state1_verify_timeout }}
+ trigger_mode: unordered
--- /dev/null
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+ start_pps: 500
+ max_pps: 500
+ duration: 300
+ step: 500
+ parallel: 1
+ max_backlog: 1000
+ repeat: "no"
+
+# Test framework
+test_timeout: 350
+test_verify_timeout: 340
--- /dev/null
+../../environments/profiling-pap-auth.yml.j2
\ No newline at end of file
--- /dev/null
+# Topology - N/A for profiling test
+
+# Routing - N/A for profiling test
+
+# Load generator configuration
+loadgen:
+ start_pps: 100
+ max_pps: 100
+ duration: 60
+ step: 100
+ parallel: 1
+ max_backlog: 1000
+ repeat: "no"
+
+# Test framework
+test_timeout: 125
+test_state1_verify_timeout: 120
+test_state2_verify_timeout: 30
--- /dev/null
+timeout: {{ test_timeout }}
+state_order: sequence
+states:
+ state_1:
+ description: >
+ Baseline profiling test
+ host:
+ profiling-server:
+ actions:
+ - execute_command:
+ command: |
+ #
+ # proto_load configuration via environment variables
+ #
+ {%- for key, value in loadgen.items() %}
+ export TEST_LOADGEN_{{ key | upper }}="{{ value }}"
+ {%- endfor %}
+ TEST_LOADGEN_NUM_MESSAGES=0
+ for ((pps=$TEST_LOADGEN_START_PPS; pps<=$TEST_LOADGEN_MAX_PPS; pps+=$TEST_LOADGEN_STEP)); do
+ TEST_LOADGEN_NUM_MESSAGES=$((TEST_LOADGEN_NUM_MESSAGES + TEST_LOADGEN_DURATION * pps))
+ done
+ export TEST_LOADGEN_NUM_MESSAGES
+ #
+ # Starting load-generator server which will generate traffic based on env configuration
+ # from above.
+ #
+ printf "Starting load-generator with the following configuration:\n"
+ {%- for key, value in loadgen.items() %}
+ printf " {{ key | upper }}:%s\n" "$TEST_LOADGEN_{{ key | upper }}"
+ {%- endfor %}
+ printf " NUM_MESSAGES: %s\n" "$TEST_LOADGEN_NUM_MESSAGES"
+
+ source /etc/freeradius/start_valgrind_profiling.sh
+
+ detach: true
+ verify:
+ timeout: {{ test_state1_verify_timeout }}
+ trigger_mode: unordered