feat(sglang-deploy): replace nginx with caddy
Assisted-by: GLM 5.3 Flash
This commit is contained in:
+188
-24
@@ -16,7 +16,11 @@ my $LOG = "$WORK/log/stubs.log";
|
||||
my $SCRIPT = $ENV{SGLANG_RIG_SCRIPT} // "$RIG/../../sglang-deploy.pl";
|
||||
my $UNIT = '/etc/systemd/system/sglang.service';
|
||||
my $ENVFILE = '/etc/sysconfig/sglang';
|
||||
my $NGINX = '/etc/nginx/conf.d/sglang.conf';
|
||||
my $CADDY = '/etc/caddy/Caddyfile.d/sglang.caddyfile';
|
||||
my $CADDY_MAIN = '/etc/caddy/Caddyfile';
|
||||
my $CADDY_UNIT = '/etc/systemd/system/caddy.service';
|
||||
my $CADDY_BIN = '/usr/local/bin/caddy';
|
||||
my $NGINX_LEGACY = '/etc/nginx/conf.d/sglang.conf';
|
||||
my $CERTDIR = '/etc/ssl/sglang';
|
||||
my $STATEDIR = '/opt/sglang';
|
||||
|
||||
@@ -105,22 +109,27 @@ sub mode_of {
|
||||
}
|
||||
|
||||
sub reset_fixture {
|
||||
for my $path ($UNIT, $ENVFILE, $NGINX) {
|
||||
for my $path ($UNIT, $ENVFILE, $CADDY, $CADDY_MAIN, $CADDY_UNIT, $CADDY_BIN,
|
||||
$NGINX_LEGACY) {
|
||||
unlink($path);
|
||||
}
|
||||
remove_tree('/etc/caddy');
|
||||
remove_tree($CERTDIR);
|
||||
remove_tree($STATEDIR);
|
||||
remove_tree($FIX);
|
||||
remove_tree($ST);
|
||||
# Directories a host that ran server-setup.pl has: nginx's configuration drop-in
|
||||
# and systemd's unit directory.
|
||||
# Directories a host that ran server-setup.pl has: the legacy nginx drop-in
|
||||
# directory an earlier release wrote into, and systemd's unit directory.
|
||||
make_dirs($FIX, $ST, "$WORK/log", '/etc/nginx/conf.d', '/etc/systemd/system');
|
||||
my $fh;
|
||||
open($fh, q{>}, $LOG) and close($fh);
|
||||
# Packages a real host or a previous run has already installed.
|
||||
# Packages a real host or a previous run has already installed. nginx stands
|
||||
# for the drop-in the previous release left behind.
|
||||
write_fixture('rpm-installed', "nginx\n");
|
||||
write_fixture('fw-services', "\n");
|
||||
# firewalld is running: server-setup.pl ensures it, and the firewall step needs it.
|
||||
# The handle is declared first: a my inside the open's argument list does not
|
||||
# reach the right-hand operand of the and on this interpreter.
|
||||
my $fw;
|
||||
open($fw, '>', "$ST/active.firewalld") and close($fw);
|
||||
return;
|
||||
@@ -184,8 +193,8 @@ sub reset_log {
|
||||
# so the script's own PATH lookup finds them and nothing of the real system is used.
|
||||
sub prepare_stubs {
|
||||
make_dirs($BIN, $FIX, $ST, "$WORK/log");
|
||||
for my $name (qw(lspci rpm dnf podman systemctl curl openssl nginx
|
||||
firewall-cmd getenforce getsebool setsebool)) {
|
||||
for my $name (qw(lspci rpm dnf podman systemctl curl openssl caddy tar useradd
|
||||
semodule firewall-cmd getenforce getsebool setsebool)) {
|
||||
my $link = "$BIN/$name";
|
||||
# A link left over from a work directory that moved reads as broken to
|
||||
# -e, and its stale target would leave the stubs unreachable: it is
|
||||
@@ -197,6 +206,21 @@ sub prepare_stubs {
|
||||
return;
|
||||
}
|
||||
|
||||
# The caddy package creates its group; the rig creates it the same way, so the
|
||||
# key permission the package makes possible is exercised for real. The group
|
||||
# file is edited directly: the minimal openEuler image carries no groupadd to
|
||||
# call, and a group entry is all the getgrnam in the script needs.
|
||||
sub prepare_group {
|
||||
return if defined getgrnam('caddy');
|
||||
my $existing = slurp_file('/etc/group');
|
||||
my $gid = 995;
|
||||
$gid++ while $existing =~ /^[^:]+:[^:]*:\Q$gid\E:/m;
|
||||
open(my $fh, '>>', '/etc/group') or die "cannot append to /etc/group: $!\n";
|
||||
print {$fh} "caddy:x:$gid:\n";
|
||||
close($fh);
|
||||
return;
|
||||
}
|
||||
|
||||
sub prepare_devices {
|
||||
# The driver creates these on a real host; the rig fakes them (needs --privileged).
|
||||
return if -e q{/dev/kfd};
|
||||
@@ -214,12 +238,15 @@ sub scenario_fresh {
|
||||
check($rc == 0, 'fresh: exit 0');
|
||||
check(-f $UNIT, 'fresh: unit written');
|
||||
check(-f $ENVFILE, 'fresh: environment file written');
|
||||
check(-f $NGINX, 'fresh: nginx configuration written');
|
||||
check(-f $CADDY, 'fresh: caddy drop-in written');
|
||||
check(-f $CADDY_MAIN, 'fresh: main Caddyfile written');
|
||||
check(-f "$CERTDIR/sglang.crt" && -f "$CERTDIR/sglang.key", 'fresh: certificate written');
|
||||
check(-d "$STATEDIR/modelscope", 'fresh: model cache directory created');
|
||||
check(mode_of($ENVFILE) eq '0600', 'fresh: environment file is 0600 (' . mode_of($ENVFILE) . ')');
|
||||
check(mode_of("$CERTDIR/sglang.key") eq '0600', 'fresh: key is 0600');
|
||||
check(mode_of("$CERTDIR/sglang.key") eq '0640', 'fresh: key is 0640 for the caddy group (' . mode_of("$CERTDIR/sglang.key") . ')');
|
||||
check(mode_of("$CERTDIR/sglang.crt") eq '0644', 'fresh: certificate is 0644');
|
||||
check((stat("$CERTDIR/sglang.key"))[5] == getgrnam('caddy'),
|
||||
'fresh: the key belongs to the caddy group');
|
||||
|
||||
my $env = slurp_file($ENVFILE);
|
||||
check_like($env, qr/^SGLANG_API_KEY=([A-Za-z0-9_-]{43})\n$/, 'fresh: generated key, url-safe, 43 chars');
|
||||
@@ -231,19 +258,29 @@ sub scenario_fresh {
|
||||
check_like($unit, qr/--mem-fraction-static 0\.9/, 'fresh: memory fraction default');
|
||||
check_unlike($unit, qr/SGLANG_USE_AITER/, 'fresh: no Radeon variables on an Instinct host');
|
||||
|
||||
my $nginx = slurp_file($NGINX);
|
||||
check_like($nginx, qr|proxy_pass http://\[::1\]:8000;|, 'fresh: nginx proxies to the loopback engine');
|
||||
check_like($nginx, qr|ssl_certificate /etc/ssl/sglang/sglang\.crt;|, 'fresh: nginx uses the sglang certificate');
|
||||
my $caddy = slurp_file($CADDY);
|
||||
check_like($caddy, qr/reverse_proxy \[::1\]:8000 \{/, 'fresh: caddy proxies to the loopback engine');
|
||||
check_like($caddy, qr|tls /etc/ssl/sglang/sglang\.crt /etc/ssl/sglang/sglang\.key|,
|
||||
'fresh: caddy uses the sglang certificate pair');
|
||||
check_like($caddy, qr/flush_interval -1/, 'fresh: streaming is unbuffered');
|
||||
my $main = slurp_file($CADDY_MAIN);
|
||||
check_like($main, qr/^import Caddyfile\.d\/\*\.caddyfile$/m, 'fresh: the main Caddyfile imports the drop-ins');
|
||||
|
||||
check(count_in_log(qr/^podman pull /) == 1, 'fresh: exactly one image pull');
|
||||
check_like(stub_log(), qr/^podman pull docker\.io\/lmsysorg\/sglang:v0\.5\.19-rocm724-mi30x$/m,
|
||||
'fresh: the pulled image is the resolved tag');
|
||||
check_like(stub_log(), qr/^caddy version$/m, 'fresh: caddy is reported from the binary');
|
||||
check_like(stub_log(), qr/^caddy validate --config \/etc\/caddy\/Caddyfile$/m,
|
||||
'fresh: the configuration is validated before the reload');
|
||||
check_like(stub_log(), qr/^systemctl enable --now caddy$/m, 'fresh: caddy enabled and started');
|
||||
check_like(stub_log(), qr/^systemctl reload caddy$/m, 'fresh: caddy reloaded');
|
||||
check(count_in_log(qr/^systemctl enable sglang$/) == 1, 'fresh: service enabled');
|
||||
check(count_in_log(qr/^systemctl start sglang$/) == 1, 'fresh: service started');
|
||||
check_like(stub_log(), qr/^firewall-cmd --permanent --add-service=https$/m, 'fresh: HTTPS opened');
|
||||
check_like($out, qr/Engine image: docker\.io\/lmsysorg\/sglang:v0\.5\.19-rocm724-mi30x/,
|
||||
'fresh: summary names the image');
|
||||
check_like($out, qr/systemd: sglang running on \[::1\]:8000/, 'fresh: summary names the endpoint');
|
||||
check_like($out, qr/Caddy: configured and reloaded/, 'fresh: summary reports caddy');
|
||||
check_like($out, qr/API key \(shown once, store it securely\)/, 'fresh: the generated key is shown once');
|
||||
check_unlike($out, qr/✗/, 'fresh: no failed step');
|
||||
return;
|
||||
@@ -261,6 +298,7 @@ sub scenario_rerun {
|
||||
check_like(stub_log(), qr/^podman image exists /m, 'rerun: the image is checked instead');
|
||||
check(count_in_log(qr/^systemctl enable sglang$/) == 0, 'rerun: no second enable');
|
||||
check(count_in_log(qr/^systemctl start sglang$/) == 0, 'rerun: no second start');
|
||||
check(count_in_log(qr/^systemctl enable --now caddy$/) == 0, 'rerun: no second caddy enable');
|
||||
check(count_in_log(qr/try-restart/) == 0, 'rerun: no restart, the unit did not change');
|
||||
check(count_in_log(qr/^dnf install /) == 0, 'rerun: no second package transaction');
|
||||
check(slurp_file($ENVFILE) eq $key_before, 'rerun: the API key is reused, not regenerated');
|
||||
@@ -282,7 +320,8 @@ sub scenario_dry_run {
|
||||
check($rc == 0, 'dry run: exit 0');
|
||||
check(!-f $UNIT, 'dry run: no unit written');
|
||||
check(!-f $ENVFILE, 'dry run: no environment file written');
|
||||
check(!-f $NGINX, 'dry run: no nginx configuration written');
|
||||
check(!-f $CADDY, 'dry run: no caddy drop-in written');
|
||||
check(!-e '/etc/caddy', 'dry run: no caddy directory created');
|
||||
check(!-d $CERTDIR, 'dry run: no certificate directory');
|
||||
check(count_in_log(qr/^podman pull /) == 0, 'dry run: no pull');
|
||||
check(count_in_log(qr/^systemctl (enable|start) /) == 0, 'dry run: no service change');
|
||||
@@ -302,14 +341,17 @@ sub scenario_uninstall {
|
||||
check($rc == 0, 'uninstall: exit 0');
|
||||
check(!-f $UNIT, 'uninstall: unit removed');
|
||||
check(!-f $ENVFILE, 'uninstall: environment file removed');
|
||||
check(!-f $NGINX, 'uninstall: nginx configuration removed');
|
||||
check(!-f $CADDY, 'uninstall: caddy drop-in removed');
|
||||
check(-f $CADDY_MAIN, 'uninstall: the main Caddyfile is kept');
|
||||
check(!-d $CERTDIR, 'uninstall: certificate directory removed');
|
||||
check(-d $STATEDIR, 'uninstall: model cache kept');
|
||||
check(-e "$ST/image.docker.io_lmsysorg_sglang_v0.5.19-rocm724-mi30x",
|
||||
'uninstall: the image is kept in podman');
|
||||
check_like(stub_log(), qr/^systemctl stop sglang$/m, 'uninstall: service stopped');
|
||||
check_like(stub_log(), qr/^systemctl disable sglang$/m, 'uninstall: service disabled');
|
||||
check_like(stub_log(), qr/^systemctl reload caddy$/m, 'uninstall: caddy reloaded after the drop-in');
|
||||
check_like($out, qr/SGLang service stopped/, 'uninstall: summary reports the stop');
|
||||
check_like($out, qr/caddy drop-in removed/, 'uninstall: summary reports the drop-in');
|
||||
check_like($out, qr/Kept on the system/, 'uninstall: the kept state is listed');
|
||||
check_like($out, qr/ModelScope cache/, 'uninstall: the cache is named as kept');
|
||||
return;
|
||||
@@ -322,6 +364,78 @@ sub scenario_uninstall_twice {
|
||||
check($rc == 0, 'uninstall twice: exit 0');
|
||||
check_like($out, qr/already absent/, 'uninstall twice: idempotent');
|
||||
check(count_in_log(qr/^systemctl stop /) == 0, 'uninstall twice: nothing to stop');
|
||||
check(count_in_log(qr/^systemctl reload caddy$/) == 0,
|
||||
'uninstall twice: no reload without a drop-in');
|
||||
return;
|
||||
}
|
||||
|
||||
# A host deployed by the nginx release carries its drop-in. Both a deploy and
|
||||
# an uninstall remove it, so exactly one proxy owns :443 afterwards.
|
||||
sub scenario_legacy_nginx {
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
open(my $fh, '>', $NGINX_LEGACY) or die "cannot write $NGINX_LEGACY: $!\n";
|
||||
print {$fh} "server {\n listen 443 ssl;\n}\n";
|
||||
close($fh);
|
||||
# The stub state: nginx is running on this host, so the removal reloads it.
|
||||
my $st;
|
||||
open($st, '>', "$ST/active.nginx") and close($st);
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check($rc == 0, 'legacy nginx: exit 0');
|
||||
check(!-f $NGINX_LEGACY, 'legacy nginx: the old drop-in is gone on deploy');
|
||||
check_like(stub_log(), qr/^systemctl reload nginx$/m,
|
||||
'legacy nginx: the running nginx is reloaded');
|
||||
check_like($out, qr/Legacy nginx configuration: removed/, 'legacy nginx: the summary names it');
|
||||
|
||||
# An uninstall on a host the new release never deployed cleans it too.
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
open($fh, '>', $NGINX_LEGACY) or die "cannot write $NGINX_LEGACY: $!\n";
|
||||
print {$fh} "server {\n listen 443 ssl;\n}\n";
|
||||
close($fh);
|
||||
reset_log();
|
||||
($rc, $out) = run_script('--uninstall');
|
||||
check($rc == 0, 'legacy nginx: uninstall exit 0');
|
||||
check(!-f $NGINX_LEGACY, 'legacy nginx: the old drop-in is gone on uninstall');
|
||||
check_like($out, qr/legacy nginx configuration removed/, 'legacy nginx: the uninstall summary names it');
|
||||
return;
|
||||
}
|
||||
|
||||
# Where no repository carries the caddy package, the official release binary
|
||||
# takes its place: download, extract, install, the service user, and the unit
|
||||
# file the package would have carried.
|
||||
sub scenario_caddy_binary {
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
write_fixture('caddy-no-package', "1\n");
|
||||
unlink('/usr/bin/caddy'); # order independence: no package binary, no stub
|
||||
unlink("$BIN/caddy") or die "cannot remove the caddy stub: $!\n";
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
|
||||
check($rc == 0, 'caddy binary: exit 0');
|
||||
check_like(stub_log(), qr/^dnf install -y caddy$/m, 'caddy binary: the package install was attempted');
|
||||
check_like(stub_log(), qr{^curl -fsSL -o \S+ https://github\.com/caddyserver/caddy/releases/download/v2\.10\.2/caddy_2\.10\.2_linux_amd64\.tar\.gz$}m,
|
||||
'caddy binary: the release asset is downloaded');
|
||||
check_like(stub_log(), qr/^tar -xzf \S+ -C \S+$/m, 'caddy binary: the archive is extracted');
|
||||
check(-x $CADDY_BIN, 'caddy binary: the binary is installed executable');
|
||||
check_like(stub_log(), qr/^useradd --system --home-dir \/var\/lib\/caddy --create-home --shell \/sbin\/nologin caddy$/m,
|
||||
'caddy binary: the service user is created');
|
||||
check(-f $CADDY_UNIT, 'caddy binary: the unit file is written');
|
||||
my $unit = slurp_file($CADDY_UNIT);
|
||||
check_like($unit, qr|ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile|,
|
||||
'caddy binary: the unit runs the release binary');
|
||||
check_like(stub_log(), qr/^systemctl daemon-reload$/m, 'caddy binary: systemd reloaded');
|
||||
check_like(stub_log(), qr{^caddy validate --config /etc/caddy/Caddyfile$}m,
|
||||
'caddy binary: the installed binary validates');
|
||||
check_like($out, qr/Caddy: configured and reloaded/, 'caddy binary: the deployment completes');
|
||||
|
||||
unlink($CADDY_BIN);
|
||||
unlink($CADDY_UNIT);
|
||||
unlink("$FIX/caddy-no-package");
|
||||
symlink("$RIG/stub.pl", "$BIN/caddy") or die "cannot restore the caddy stub: $!\n";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -495,7 +609,7 @@ sub scenario_validation {
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--port', '443', '--dry-run');
|
||||
check($rc == 1, 'validation: port 443 refused');
|
||||
check_like($out, qr/must be 1-65535 and not 443/, 'validation: port message');
|
||||
check_like($out, qr/must be an integer 1-65535 and not 443/, 'validation: port message');
|
||||
($rc, $out) = run_script('--model', 'not-a-model-id', '--dry-run');
|
||||
check($rc == 1, 'validation: bad model ID refused');
|
||||
check_like($out, qr/Invalid model ID/, 'validation: model message');
|
||||
@@ -545,7 +659,7 @@ sub scenario_non_root {
|
||||
my $rc = $? >> 8;
|
||||
my $out = slurp_file($out_file);
|
||||
check($rc == 0, 'non-root: --version works without root');
|
||||
check_like($out, qr/^sglang-deploy\.pl 2\.0\.0$/, 'non-root: the version is printed');
|
||||
check_like($out, qr/^sglang-deploy\.pl 2\.1\.0$/, 'non-root: the version is printed');
|
||||
|
||||
my $pid3 = fork();
|
||||
die "cannot fork: $!\n" unless defined $pid3;
|
||||
@@ -583,12 +697,12 @@ sub scenario_dependency_section {
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
write_fixture('rpm-installed', "pciutils\ncurl\nopenssl\npodman\nnginx\n");
|
||||
write_fixture('rpm-installed', "pciutils\ncurl\nopenssl\npodman\ntar\n");
|
||||
reset_log();
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--dry-run');
|
||||
check($rc == 0, 'deps: exit 0');
|
||||
check(count_in_log(qr/^dnf install /) == 0, 'deps: no transaction when all packages are present');
|
||||
check_like($out, qr/All 4 packages already installed/, 'deps: reported as present');
|
||||
check_like($out, qr/All 5 packages already installed/, 'deps: reported as present');
|
||||
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
|
||||
@@ -596,10 +710,41 @@ sub scenario_dependency_section {
|
||||
write_fixture('rpm-installed', "\n");
|
||||
reset_log();
|
||||
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check_like(stub_log(), qr/^dnf install -y pciutils curl openssl podman$/m,
|
||||
'deps: the four packages are installed in one transaction');
|
||||
check_like($out, qr/nginx \(reverse proxy\) is not installed: run server-setup.pl first/,
|
||||
'deps: the missing reverse proxy is warned about');
|
||||
check_like(stub_log(), qr/^dnf install -y pciutils curl openssl podman tar$/m,
|
||||
'deps: the five packages are installed in one transaction');
|
||||
check_like(stub_log(), qr/^caddy version$/m, 'deps: caddy is checked after the transaction');
|
||||
check_unlike($out, qr/run server-setup\.pl first/,
|
||||
'deps: no warning points at server-setup.pl, caddy is installed here');
|
||||
return;
|
||||
}
|
||||
|
||||
# CentOS Stream carries caddy in EPEL, and the repository file installs first,
|
||||
# which is what makes the package transaction below it resolvable.
|
||||
sub scenario_epel {
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
my $real = slurp_file('/etc/os-release');
|
||||
open(my $fh, '>', '/etc/os-release') or die "cannot write /etc/os-release: $!\n";
|
||||
print {$fh} "ID=centos\nVERSION_ID=\"10\"\n";
|
||||
close($fh);
|
||||
unlink('/usr/bin/caddy'); # order independence: no binary, no stub
|
||||
unlink("$BIN/caddy");
|
||||
reset_log();
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
open(my $back, '>', '/etc/os-release') or die "cannot restore /etc/os-release: $!\n";
|
||||
print {$back} $real;
|
||||
close($back);
|
||||
|
||||
check($rc == 0, 'epel: exit 0');
|
||||
check_like(stub_log(), qr/^dnf install -y epel-release$/m, 'epel: the repository file installs first');
|
||||
check_like(stub_log(), qr/^dnf install -y caddy$/m, 'epel: the package installs after it');
|
||||
check_like($out, qr/Installing caddy\.\.\. package/, 'epel: the package path is taken');
|
||||
check_like($out, qr/Caddy: configured and reloaded/, 'epel: the deployment completes');
|
||||
|
||||
unlink('/usr/bin/caddy');
|
||||
unlink("$FIX/caddy-no-package");
|
||||
symlink("$RIG/stub.pl", "$BIN/caddy") or die "cannot restore the caddy stub: $!\n";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -619,7 +764,7 @@ sub scenario_custom_layout {
|
||||
);
|
||||
check($rc == 0, 'custom layout: exit 0');
|
||||
check(-f '/etc/systemd/system/llm.service', 'custom layout: the named unit is written');
|
||||
check(-f '/etc/nginx/conf.d/llm.conf', 'custom layout: the named nginx file is written');
|
||||
check(-f '/etc/caddy/Caddyfile.d/llm.caddyfile', 'custom layout: the named caddy drop-in is written');
|
||||
# The certificate file names follow the program, as they did before, not the
|
||||
# service name; the directory follows --cert-dir.
|
||||
check(-f '/etc/ssl/llm/sglang.crt', 'custom layout: certificates follow the directory');
|
||||
@@ -632,7 +777,7 @@ sub scenario_custom_layout {
|
||||
check_like($unit, qr|--volume /srv/llm/modelscope:/root/\.cache/modelscope:Z|,
|
||||
'custom layout: the cache volume follows the state directory');
|
||||
unlink('/etc/systemd/system/llm.service');
|
||||
unlink('/etc/nginx/conf.d/llm.conf');
|
||||
unlink('/etc/caddy/Caddyfile.d/llm.caddyfile');
|
||||
remove_tree('/etc/ssl/llm');
|
||||
remove_tree('/srv/llm');
|
||||
return;
|
||||
@@ -674,17 +819,32 @@ sub scenario_selinux {
|
||||
check(count_in_log(qr/^setsebool /) == 0, 'selinux: nothing set while permissive');
|
||||
check_like($out, qr/SELinux: permissive \(skipped\)/, 'selinux: reported as skipped');
|
||||
|
||||
# Enforcing with no confined caddy policy: the distributions run caddy
|
||||
# unconfined, so no boolean is touched.
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
$ENV{STUB_SELINUX} = 'Enforcing';
|
||||
reset_log();
|
||||
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check(count_in_log(qr/^setsebool /) == 0, 'selinux: no boolean without a confined policy');
|
||||
check_like($out, qr/SELinux: caddy runs unconfined \(nothing to do\)/,
|
||||
'selinux: the unconfined case is stated');
|
||||
|
||||
# Enforcing with a confined caddy policy loaded: the boolean is set.
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1]\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
write_fixture('semodule-caddy', "1\n");
|
||||
reset_log();
|
||||
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check_like(stub_log(), qr/^semodule -l$/m, 'selinux: the loaded modules are asked for');
|
||||
check_like(stub_log(), qr/^setsebool -P httpd_can_network_connect=1$/m, 'selinux: the boolean is set');
|
||||
check_like($out, qr/SELinux: httpd_can_network_connect on/, 'selinux: reported as on');
|
||||
reset_log();
|
||||
my ($rc2, $out2) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check(count_in_log(qr/^setsebool /) == 0, 'selinux: nothing set when already on');
|
||||
unlink("$FIX/semodule-caddy");
|
||||
delete $ENV{STUB_SELINUX};
|
||||
return;
|
||||
}
|
||||
@@ -695,6 +855,9 @@ my %scenarios = (
|
||||
dry_run => \&scenario_dry_run,
|
||||
uninstall => \&scenario_uninstall,
|
||||
uninstall_twice => \&scenario_uninstall_twice,
|
||||
legacy_nginx => \&scenario_legacy_nginx,
|
||||
caddy_binary => \&scenario_caddy_binary,
|
||||
epel => \&scenario_epel,
|
||||
radeon => \&scenario_radeon,
|
||||
radeon_dev => \&scenario_radeon_dev,
|
||||
radeon_with_image => \&scenario_radeon_with_image,
|
||||
@@ -715,6 +878,7 @@ my %scenarios = (
|
||||
);
|
||||
|
||||
prepare_stubs();
|
||||
prepare_group();
|
||||
prepare_devices();
|
||||
|
||||
my @wanted = @ARGV ? @ARGV : sort keys %scenarios;
|
||||
|
||||
Regular → Executable
+71
-5
@@ -74,6 +74,12 @@ if ($name eq 'rpm') {
|
||||
|
||||
if ($name eq 'dnf') {
|
||||
my @pkgs = grep { !/^-/ && $_ ne 'install' } @args;
|
||||
# Parenthesised on purpose: a named list operator swallows a trailing &&,
|
||||
# and the unparenthesised form asks the grep about a boolean, not the list.
|
||||
if ((grep { $_ eq 'caddy' } @pkgs) && -f "$FIX/caddy-no-package") {
|
||||
print STDERR "Error: Unable to find a match: caddy\n";
|
||||
exit 1;
|
||||
}
|
||||
my $installed = fixture_text('rpm-installed', '');
|
||||
for my $pkg (@pkgs) {
|
||||
$installed .= "$pkg\n" unless $installed =~ /^\Q$pkg\E$/m;
|
||||
@@ -82,6 +88,12 @@ if ($name eq 'dnf') {
|
||||
print {$fh} $installed;
|
||||
close($fh);
|
||||
}
|
||||
# A package transaction that installs caddy leaves the binary where PATH
|
||||
# finds it, the way the real package does. The grep is parenthesised as
|
||||
# above: a named list operator swallows a trailing &&.
|
||||
if ((grep { $_ eq 'caddy' } @pkgs) && !-e '/usr/bin/caddy') {
|
||||
symlink($0, '/usr/bin/caddy');
|
||||
}
|
||||
print "Installing: @pkgs\n";
|
||||
exit 0;
|
||||
}
|
||||
@@ -138,14 +150,32 @@ if ($name eq 'curl') {
|
||||
print "\n__HTTP__$code\n" if $joined =~ /__HTTP__/;
|
||||
exit 0;
|
||||
}
|
||||
if ($url =~ m{api\.github\.com/repos/caddyserver/caddy}) {
|
||||
print fixture_text('caddy-release.json', qq({"tag_name":"v2.10.2"}\n));
|
||||
exit 0;
|
||||
}
|
||||
if ($url =~ m{api\.github\.com}) {
|
||||
print fixture_text('releases.json', qq({"tag_name":"v0.5.19"}\n));
|
||||
exit 0;
|
||||
}
|
||||
if ($url =~ m{github\.com/caddyserver/caddy/releases/download/}) {
|
||||
my $dest;
|
||||
for my $i (0 .. $#args) {
|
||||
$dest = $args[$i + 1] if $args[$i] eq '-o';
|
||||
}
|
||||
if (defined $dest) {
|
||||
open(my $fh, '>', $dest) or exit 1;
|
||||
print {$fh} "stub caddy release archive\n";
|
||||
close($fh);
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
if ($url =~ m{hub\.docker\.com}) {
|
||||
if (-f "$FIX/image-missing") {
|
||||
print STDERR "curl: (22) The requested URL returned error: 404\n";
|
||||
exit 22;
|
||||
# A definitive 404 on a tag lookup is what the script reads as
|
||||
# unpublished; a curl-level failure would read as cannot-tell.
|
||||
if ($url =~ m{/tags/[A-Za-z0-9._-]+$} && -f "$FIX/image-missing") {
|
||||
print "404";
|
||||
exit 0;
|
||||
}
|
||||
if ($url =~ /tags\?/) {
|
||||
# A tag listing, newest first: the rig's AMD development build.
|
||||
@@ -181,8 +211,44 @@ if ($name eq 'openssl') {
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'nginx') {
|
||||
print "nginx: configuration file /etc/nginx/nginx.conf test is successful\n";
|
||||
if ($name eq 'caddy') {
|
||||
my $joined = join(' ', @args);
|
||||
if ($joined =~ /version/) {
|
||||
print "v2.10.2 h1:stub\n";
|
||||
exit 0;
|
||||
}
|
||||
if ($joined =~ /validate/) {
|
||||
print "Valid configuration\n";
|
||||
exit 0;
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'tar') {
|
||||
# The release-binary install extracts the archive and installs the binary it
|
||||
# names; the stub lays down a link to this dispatcher, so the installed
|
||||
# stand-in answers and logs like every other stubbed command.
|
||||
my $dest_dir;
|
||||
for my $i (0 .. $#args) {
|
||||
$dest_dir = $args[$i + 1] if $args[$i] eq '-C';
|
||||
}
|
||||
if (defined $dest_dir) {
|
||||
unlink("$dest_dir/caddy");
|
||||
symlink($0, "$dest_dir/caddy");
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'useradd') {
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'semodule') {
|
||||
# A loaded module line looks like "100 caddy\tpp"; the fixture decides
|
||||
# whether this host carries a confined caddy policy.
|
||||
if (-f "$FIX/semodule-caddy") {
|
||||
print "100 caddy\tpp\n";
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
|
||||
|
||||
+37
-16
@@ -138,7 +138,7 @@ is((valid_dir_path('/opt/sg lang') ? 1 : 0), 0, 'valid_dir_path: whitespace is r
|
||||
is((valid_dir_path('/opt/%h/sglang') ? 1 : 0), 0,
|
||||
'valid_dir_path: a systemd specifier is refused');
|
||||
is((valid_dir_path('/opt/x;/sglang') ? 1 : 0), 0,
|
||||
'valid_dir_path: an nginx directive end is refused');
|
||||
'valid_dir_path: a path with a semicolon is refused');
|
||||
|
||||
# ---- run(): exit status, signals and timeout ------------------------------
|
||||
my $killed = run([$^X, '-e', 'kill 9, $$']);
|
||||
@@ -164,22 +164,43 @@ is(scalar @{ radeon_env_for(undef, 1) }, 2,
|
||||
'env: a custom image on a Radeon-only host carries the Radeon defaults');
|
||||
is(scalar @{ radeon_env_for('mi30x', 0) }, 0, 'env: an Instinct host carries none');
|
||||
|
||||
# ---- nginx config --------------------------------------------------------
|
||||
my $conf = nginx_conf_content(8000, '[::1]', '/etc/ssl/sglang');
|
||||
like($conf, qr/listen \[::\]:443 ssl;/, 'nginx: IPv6 listener on a dual-stack kernel');
|
||||
like($conf, qr/listen 443 ssl;/, 'nginx: IPv4 listener');
|
||||
like($conf, qr|ssl_certificate /etc/ssl/sglang/sglang\.crt;|, 'nginx: certificate path');
|
||||
like($conf, qr/ssl_certificate_key \/etc\/ssl\/sglang\/sglang\.key;/, 'nginx: key path');
|
||||
like($conf, qr|proxy_pass http://\[::1\]:8000;|, 'nginx: loopback upstream with the port');
|
||||
like($conf, qr/proxy_http_version 1\.1;/, 'nginx: HTTP/1.1 for streaming');
|
||||
like($conf, qr/proxy_buffering off;/, 'nginx: buffering off for streaming');
|
||||
like($conf, qr/proxy_set_header Host \$host;/, 'nginx: $host survives the heredoc');
|
||||
like($conf, qr/proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;/,
|
||||
'nginx: $proxy_add_x_forwarded_for survives the heredoc');
|
||||
# ---- caddy drop-in --------------------------------------------------------
|
||||
my $conf = caddyfile_content(8000, '[::1]', '/etc/ssl/sglang');
|
||||
like($conf, qr/^:443 \{$/m, 'caddy: the endpoint site on 443');
|
||||
like($conf, qr|tls /etc/ssl/sglang/sglang\.crt /etc/ssl/sglang/sglang\.key|,
|
||||
'caddy: certificate pair paths');
|
||||
like($conf, qr/reverse_proxy \[::1\]:8000 \{/, 'caddy: loopback upstream with the port');
|
||||
like($conf, qr/flush_interval -1/, 'caddy: unbuffered streaming');
|
||||
like($conf, qr/header_up X-Real-IP \{remote_host\}/, 'caddy: X-Real-IP survives the heredoc');
|
||||
like($conf, qr/max_size 50MB/, 'caddy: the request body limit');
|
||||
like($conf, qr/\treverse_proxy /, 'caddy: tab-indented as the Caddyfile is formatted');
|
||||
check_unlike($conf, qr/\bbind\b/, 'caddy: no bind directive, the kernel decides the families');
|
||||
check_unlike($conf, qr/acme|on_demand|http:\/\/\{/i, 'caddy: no ACME, the self-signed pair is used');
|
||||
|
||||
my $conf4 = nginx_conf_content(8000, '127.0.0.1', '/etc/ssl/sglang');
|
||||
is($conf4 =~ /\[::\]/ ? 'yes' : 'no',
|
||||
(-e '/proc/net/if_inet6' ? 'yes' : 'no'), 'nginx: IPv6 listener follows the kernel');
|
||||
my $conf4 = caddyfile_content(9000, '127.0.0.1', '/etc/ssl/llm');
|
||||
like($conf4, qr/reverse_proxy 127\.0\.0\.1:9000 \{/, 'caddy: an IPv4 upstream carries the port');
|
||||
like($conf4, qr|tls /etc/ssl/llm/sglang\.crt|, 'caddy: the certificate directory follows --cert-dir');
|
||||
is(caddyfile_path('sglang'), '/etc/caddy/Caddyfile.d/sglang.caddyfile',
|
||||
'caddy: the drop-in path follows the service name');
|
||||
is(caddy_main_config(), '/etc/caddy/Caddyfile', 'caddy: the main Caddyfile path');
|
||||
|
||||
# ---- caddy release binary -------------------------------------------------
|
||||
is(uname_to_arch('x86_64'), 'amd64', 'caddy binary: x86_64 maps to amd64');
|
||||
is(uname_to_arch('aarch64'), 'arm64', 'caddy binary: aarch64 maps to arm64');
|
||||
is(uname_to_arch('ppc64le'), undef, 'caddy binary: an unmapped machine is refused');
|
||||
is(caddy_asset_url('2.10.2', 'amd64'),
|
||||
'https://github.com/caddyserver/caddy/releases/download/v2.10.2/caddy_2.10.2_linux_amd64.tar.gz',
|
||||
'caddy binary: the release asset URL');
|
||||
|
||||
my $caddy_unit = caddy_unit_content();
|
||||
like($caddy_unit, qr|ExecStartPre=/usr/local/bin/caddy validate --config /etc/caddy/Caddyfile|,
|
||||
'caddy binary: the unit validates before it starts');
|
||||
like($caddy_unit, qr|ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile|,
|
||||
'caddy binary: the unit runs the release binary');
|
||||
like($caddy_unit, qr|ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile|,
|
||||
'caddy binary: the unit reloads through the admin endpoint');
|
||||
like($caddy_unit, qr/^User=caddy$/m, 'caddy binary: the unit runs as the caddy user');
|
||||
like($caddy_unit, qr/AmbientCapabilities=CAP_NET_BIND_SERVICE/, 'caddy binary: the port capability');
|
||||
|
||||
# ---- systemd unit --------------------------------------------------------
|
||||
my $unit = systemd_content({
|
||||
|
||||
Reference in New Issue
Block a user