feat: initial release of the scripts collection
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,732 @@
|
||||
#!/usr/bin/perl
|
||||
# Rig for sglang-deploy.pl: runs the real script as root inside a container against
|
||||
# stub commands, and checks the state, the written files and the exit codes it leaves.
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
# The rig file lives beside this repository's other checks, everything it writes lives
|
||||
# in a work directory that is bind mounted into the container, and the script under test
|
||||
# is the one in the repository root.
|
||||
my $RIG = $0 =~ m{^(.*)/[^/]+$} ? $1 : '.';
|
||||
my $WORK = $ENV{SGLANG_RIG_WORK} // '/tmp/sglang-rig';
|
||||
my $BIN = "$WORK/bin";
|
||||
my $FIX = "$WORK/fixture";
|
||||
my $ST = "$WORK/state";
|
||||
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 $CERTDIR = '/etc/ssl/sglang';
|
||||
my $STATEDIR = '/opt/sglang';
|
||||
|
||||
my ($passed, $failed) = (0, 0);
|
||||
my @failures;
|
||||
|
||||
sub check {
|
||||
my ($cond, $label) = @_;
|
||||
if (!defined $label || $label eq '') {
|
||||
my @stack;
|
||||
for my $i (0 .. 3) {
|
||||
my @c = caller($i);
|
||||
push @stack, join(':', $c[1] // '?', $c[2] // '?');
|
||||
}
|
||||
$label = 'no label, stack ' . join(' <- ', @stack);
|
||||
}
|
||||
if ($cond) { $passed++; print "ok $label\n"; return 1 }
|
||||
$failed++;
|
||||
push @failures, $label;
|
||||
print "FAIL $label\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
# The verdict is forced into a boolean: a bare match in a sub argument list is a list
|
||||
# context, where a failed match yields the empty list and would shift the label into
|
||||
# the condition slot, turning a failure into a silent pass.
|
||||
sub check_like {
|
||||
my ($text, $re, $label) = @_;
|
||||
$label = 'check_like without a label' unless defined $label;
|
||||
my $matched = (defined $text && $text =~ $re) ? 1 : 0;
|
||||
return check($matched, $label);
|
||||
}
|
||||
|
||||
sub check_unlike {
|
||||
my ($text, $re, $label) = @_;
|
||||
$label = 'check_unlike without a label' unless defined $label;
|
||||
my $matched = (defined $text && $text !~ $re) ? 1 : 0;
|
||||
return check($matched, $label);
|
||||
}
|
||||
|
||||
# Directories are made and removed with the language's own calls, so no module has to
|
||||
# be installed on the machine running the rig.
|
||||
sub make_dirs {
|
||||
for my $path (@_) {
|
||||
next if -d $path;
|
||||
my @parts = split m{/}, $path;
|
||||
my $built = '';
|
||||
for my $part (@parts) {
|
||||
next unless length $part;
|
||||
$built .= "/$part";
|
||||
mkdir($built, 0755) unless -d $built;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub remove_tree {
|
||||
my ($path) = @_;
|
||||
return unless -d $path;
|
||||
if (opendir(my $dh, $path)) {
|
||||
for my $entry (readdir($dh)) {
|
||||
next if $entry eq '.' || $entry eq '..';
|
||||
my $child = "$path/$entry";
|
||||
remove_tree($child) if -d $child;
|
||||
unlink($child);
|
||||
}
|
||||
closedir($dh);
|
||||
}
|
||||
rmdir($path);
|
||||
return;
|
||||
}
|
||||
|
||||
sub slurp_file {
|
||||
my ($path) = @_;
|
||||
return '' unless -f $path;
|
||||
open(my $fh, '<', $path) or return '';
|
||||
local $/ = undef;
|
||||
my $text = <$fh>;
|
||||
close($fh);
|
||||
return defined $text ? $text : '';
|
||||
}
|
||||
|
||||
sub mode_of {
|
||||
my ($path) = @_;
|
||||
return sprintf('%04o', (stat($path))[2] & 07777);
|
||||
}
|
||||
|
||||
sub reset_fixture {
|
||||
for my $path ($UNIT, $ENVFILE, $NGINX) {
|
||||
unlink($path);
|
||||
}
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
my $fw;
|
||||
open($fw, '>', "$ST/active.firewalld") and close($fw);
|
||||
return;
|
||||
}
|
||||
|
||||
sub write_fixture {
|
||||
my ($name, $content) = @_;
|
||||
open(my $fh, '>', "$FIX/$name") or die "cannot write $FIX/$name: $!\n";
|
||||
print {$fh} $content;
|
||||
close($fh);
|
||||
return;
|
||||
}
|
||||
|
||||
sub fixture_exists {
|
||||
my ($name) = @_;
|
||||
return -f "$FIX/$name" ? 1 : 0;
|
||||
}
|
||||
|
||||
# Run the script with the stub PATH and return (rc, merged output).
|
||||
sub run_script {
|
||||
my (@args) = @_;
|
||||
my $out_file = "$WORK/log/run.out";
|
||||
my $pid = fork();
|
||||
die "cannot fork: $!\n" unless defined $pid;
|
||||
if ($pid == 0) {
|
||||
open(STDOUT, '>', $out_file);
|
||||
open(STDERR, '>&', \*STDOUT);
|
||||
$ENV{PATH} = "$BIN:/usr/bin:/bin";
|
||||
# The stubs read the work directory from the environment, since they are
|
||||
# reached through links and cannot tell where the rig file lives.
|
||||
$ENV{STUB_WORK} = $WORK;
|
||||
exec '/usr/bin/perl', $SCRIPT, @args;
|
||||
exit 126;
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
my $rc = $? >> 8;
|
||||
return ($rc, slurp_file($out_file));
|
||||
}
|
||||
|
||||
sub stub_log {
|
||||
return slurp_file($LOG);
|
||||
}
|
||||
|
||||
sub count_in_log {
|
||||
my ($pattern) = @_;
|
||||
my @lines = grep { /$pattern/ } split /\n/, stub_log();
|
||||
return scalar @lines;
|
||||
}
|
||||
|
||||
sub reset_log {
|
||||
my $fh;
|
||||
open($fh, q{>}, $LOG) and close($fh);
|
||||
return;
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenarios
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# The stubs are one dispatcher file reached through links named after each command,
|
||||
# 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)) {
|
||||
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
|
||||
# replaced rather than kept or died on.
|
||||
if (-l $link && !-e $link) { unlink($link) }
|
||||
next if -e $link;
|
||||
symlink("$RIG/stub.pl", $link) or die "cannot link $link: $!\n";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub prepare_devices {
|
||||
# The driver creates these on a real host; the rig fakes them (needs --privileged).
|
||||
return if -e q{/dev/kfd};
|
||||
system(qw(mknod /dev/kfd c 237 0));
|
||||
mkdir(q{/dev/dri}, 0755) unless -d q{/dev/dri};
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_fresh {
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1] (rev 01)\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
|
||||
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 "$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.crt") eq '0644', 'fresh: certificate is 0644');
|
||||
|
||||
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');
|
||||
|
||||
my $unit = slurp_file($UNIT);
|
||||
check_like($unit, qr|docker\.io/lmsysorg/sglang:v0\.5\.19-rocm724-mi30x|, 'fresh: image resolved from ROCm 7.2.4 and MI300');
|
||||
check_like($unit, qr/--tp-size 1/, 'fresh: tensor parallel default');
|
||||
check_like($unit, qr/--context-length 4096/, 'fresh: context length default');
|
||||
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');
|
||||
|
||||
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(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/API key \(shown once, store it securely\)/, 'fresh: the generated key is shown once');
|
||||
check_unlike($out, qr/✗/, 'fresh: no failed step');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_rerun {
|
||||
scenario_fresh();
|
||||
reset_log();
|
||||
my $key_before = slurp_file($ENVFILE);
|
||||
my $unit_before = slurp_file($UNIT);
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
|
||||
check($rc == 0, 'rerun: exit 0');
|
||||
check(count_in_log(qr/^podman pull /) == 0, 'rerun: no second pull');
|
||||
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/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');
|
||||
check(slurp_file($UNIT) eq $unit_before, 'rerun: the unit file is byte-identical');
|
||||
check_like($out, qr/API key: existing key reused/, 'rerun: the key is reported as reused');
|
||||
check_like($out, qr/Engine image: already present/, 'rerun: the image is reported present');
|
||||
check_like($out, qr/systemd: sglang running/, 'rerun: the service is reported running');
|
||||
check_unlike($out, qr/would /, 'rerun: nothing is phrased as would');
|
||||
check_unlike($out, qr/✗/, 'rerun: no failed step');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_dry_run {
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI300X OAM [1002:74a1] (rev 01)\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--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(!-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');
|
||||
check(count_in_log(qr/^dnf install /) == 0, 'dry run: no package transaction');
|
||||
check_like($out, qr/DRY RUN: no changes will be made/, 'dry run: banner');
|
||||
check_like($out, qr/would be pulled|would be installed/, 'dry run: phrased as would');
|
||||
check_like($out, qr|Engine image: docker\.io/lmsysorg/sglang:v0\.5\.19-rocm724-mi30x|,
|
||||
'dry run: the resolved image is reported');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_uninstall {
|
||||
scenario_fresh();
|
||||
reset_log();
|
||||
my ($rc, $out) = run_script('--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(!-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($out, qr/SGLang service stopped/, 'uninstall: summary reports the stop');
|
||||
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;
|
||||
}
|
||||
|
||||
sub scenario_uninstall_twice {
|
||||
scenario_uninstall();
|
||||
reset_log();
|
||||
my ($rc, $out) = run_script('--uninstall');
|
||||
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');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_radeon {
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 31 [Radeon RX 7900 XTX] [1002:744c] (rev c8)\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
|
||||
check($rc == 1, 'radeon: exit 1');
|
||||
check_like($out, qr/Radeon card detected/, 'radeon: names the cause');
|
||||
check_like($out, qr/rocm\.Dockerfile/, 'radeon: names the build recipe');
|
||||
check_like($out, qr/--image sglang-rocm:latest/, 'radeon: names the way forward');
|
||||
check(!-f $UNIT, 'radeon: nothing deployed');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_radeon_dev {
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Strix Halo [Radeon Graphics / Radeon 8050S Graphics / Radeon 8060S Graphics] [1002:1586] (rev d1)\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
|
||||
check($rc == 0, 'radeon dev: exit 0');
|
||||
check_like(stub_log(), qr{^curl .*rocm/sgl-dev/tags\?.*$}m,
|
||||
'radeon dev: the AMD tag index is asked for the newest build');
|
||||
my $unit = slurp_file($UNIT);
|
||||
check_like($unit, qr|docker\.io/rocm/sgl-dev:v0\.5\.19-rocm724-gfx1151-20260916|,
|
||||
'radeon dev: the newest dated build is deployed');
|
||||
check_like($unit, qr/Environment=SGLANG_USE_AITER=false/, 'radeon dev: AITER off');
|
||||
check_like($unit, qr/Environment=SGLANG_ROCM_FUSED_DECODE_MLA=false/,
|
||||
'radeon dev: fused decode MLA off');
|
||||
check_like($out, qr/AMD publishes this build daily/, 'radeon dev: the moving tag is stated');
|
||||
check_like($out, qr/the only flavour published for gfx1151/, 'radeon dev: the flavour is explained');
|
||||
|
||||
reset_log();
|
||||
my ($rc2, $out2) = run_script('--model', 'ZhipuAI/GLM-5.3', '--image', 'localhost/pinned:1');
|
||||
check_like(slurp_file($UNIT), qr|localhost/pinned:1|, 'radeon dev: --image pins a build');
|
||||
check(count_in_log(qr{^curl .*rocm/sgl-dev/tags\?.*$}) == 0,
|
||||
'radeon dev: no tag resolution when --image is given');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_radeon_with_image {
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Strix Halo [Radeon 8060S] [1002:150e]\n");
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--image', 'localhost/sglang-rocm:gfx1151');
|
||||
|
||||
check($rc == 0, 'radeon with image: exit 0');
|
||||
my $unit = slurp_file($UNIT);
|
||||
check_like($unit, qr/Environment=SGLANG_USE_AITER=false/, 'radeon with image: AITER off');
|
||||
check_like($unit, qr/Environment=SGLANG_ROCM_FUSED_DECODE_MLA=false/,
|
||||
'radeon with image: fused decode MLA off');
|
||||
check_like($unit, qr|localhost/sglang-rocm:gfx1151|, 'radeon with image: the requested image');
|
||||
my ($rc2, $out2) = run_script('--model', 'ZhipuAI/GLM-5.3', '--image', 'localhost/sglang-rocm:gfx1151');
|
||||
check($rc2 == 0, 'radeon with image: rerun exit 0');
|
||||
check(slurp_file($UNIT) eq $unit, 'radeon with image: the unit is unchanged on a rerun');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_rocm_flavours {
|
||||
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.3\n");
|
||||
my ($rc, $out) = run_script('--dry-run', '--model', 'ZhipuAI/GLM-5.3');
|
||||
check_like($out, qr/rocm720-mi30x/, 'flavour: ROCm 7.2.3 takes rocm720');
|
||||
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', "10.0.0\n");
|
||||
($rc, $out) = run_script('--dry-run', '--model', 'ZhipuAI/GLM-5.3');
|
||||
check_like($out, qr/rocm10-mi30x/, 'flavour: ROCm 10.0.0 takes rocm10');
|
||||
reset_fixture();
|
||||
write_fixture('gpu.txt', "03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Instinct MI350X [1002:75a0]\n");
|
||||
write_fixture('rocm.txt', "7.2.4\n");
|
||||
($rc, $out) = run_script('--dry-run', '--model', 'ZhipuAI/GLM-5.3');
|
||||
check_like($out, qr/rocm724-mi35x/, 'flavour: MI350X takes mi35x');
|
||||
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', "6.4.0\n");
|
||||
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check($rc == 1, 'flavour: an old host ROCm is refused');
|
||||
check_like($out, qr/No published image targets ROCm 6\.4\.0/, 'flavour: the refusal names the version');
|
||||
check_like($out, qr/--image/, 'flavour: the refusal names the way forward');
|
||||
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");
|
||||
($rc, $out) = run_script('--dry-run', '--model', 'ZhipuAI/GLM-5.3');
|
||||
check_like($out, qr/rocm10-mi30x/, 'flavour: no host ROCm takes the newest');
|
||||
check_like($out, qr/No ROCm userland found on the host/, 'flavour: the assumption is stated out loud');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_modelscope {
|
||||
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('ms.code', "200\n");
|
||||
write_fixture('ms.json', qq({"Data":{"Name":"GLM-5.3"}}\n));
|
||||
my ($rc, $out) = run_script('--dry-run', '--model', 'ZhipuAI/GLM-5.3');
|
||||
check_like($out, qr/Verifying GLM 5\.3 on ModelScope\.\.\. found/, 'modelscope: found');
|
||||
|
||||
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('ms.code', "404\n");
|
||||
($rc, $out) = run_script('--model', 'nope/does-not-exist');
|
||||
check($rc == 1, 'missing model: exit 1');
|
||||
check_like($out, qr/does not exist on ModelScope/, 'missing model: named as missing');
|
||||
|
||||
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");
|
||||
# A 200 without a Name carries no usable answer: the deployment proceeds with
|
||||
# the probe marked unverified rather than blocked.
|
||||
write_fixture('ms.code', "200\n");
|
||||
write_fixture('ms.json', qq({}\n));
|
||||
($rc, $out) = run_script('--dry-run', '--model', 'ZhipuAI/GLM-5.3');
|
||||
check($rc == 0, 'modelscope: an unnamed answer does not block');
|
||||
check_like($out, qr/could not verify/, 'modelscope: the unnamed answer reads as unverified');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_hardware {
|
||||
reset_fixture();
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check($rc == 1, 'no GPU: exit 1');
|
||||
check_like($out, qr/No AMD GPU detected/, 'no GPU: named as missing');
|
||||
|
||||
# The missing driver is checked outside the rig: podman bind-mounts /dev/kfd into a
|
||||
# privileged container, so it cannot be removed from inside one.
|
||||
return;
|
||||
}
|
||||
|
||||
# Run without a GPU device node (a container without --privileged, or a host whose
|
||||
# driver is not loaded): the script must refuse and say what is missing.
|
||||
sub scenario_no_driver {
|
||||
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");
|
||||
if (-e '/dev/kfd') {
|
||||
print "skip no driver: this run has the device node (needs a run without --privileged)\n";
|
||||
return;
|
||||
}
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check($rc == 1, 'no driver: exit 1');
|
||||
check_like($out, qr/amdgpu kernel driver is not ready/, 'no driver: named as missing');
|
||||
check_like($out, qr{/dev/kfd}, 'no driver: names the device node');
|
||||
check(!-f $UNIT, 'no driver: nothing deployed');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_bad_os {
|
||||
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('os-release', "ID=ubuntu\nVERSION_ID=\"24.04\"\n");
|
||||
my $real = slurp_file('/etc/os-release');
|
||||
open(my $fh, '>', '/etc/os-release') or die "cannot write /etc/os-release: $!\n";
|
||||
print {$fh} slurp_file("$FIX/os-release");
|
||||
close($fh);
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--dry-run');
|
||||
open(my $back, '>', '/etc/os-release') or die "cannot restore /etc/os-release: $!\n";
|
||||
print {$back} $real;
|
||||
close($back);
|
||||
|
||||
check($rc == 1, 'bad OS: exit 1');
|
||||
check_like($out, qr/Unsupported operating system: 'ubuntu'/, 'bad OS: names the system');
|
||||
check_like($out, qr/Supported systems: Fedora, CentOS Stream, openEuler/, 'bad OS: lists the supported ones');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_validation {
|
||||
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");
|
||||
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');
|
||||
($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');
|
||||
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--gpu-memory-utilization', '1.5', '--dry-run');
|
||||
check($rc == 1, 'validation: memory fraction above 1 refused');
|
||||
check_like($out, qr/must be in \(0, 1\]/, 'validation: memory fraction message');
|
||||
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--api-key', 'has space', '--dry-run');
|
||||
check($rc == 1, 'validation: whitespace API key refused');
|
||||
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--rocm-flavour', 'rocm999', '--dry-run');
|
||||
check($rc == 1, 'validation: unknown flavour refused');
|
||||
check_like($out, qr/Invalid --rocm-flavour/, 'validation: flavour message');
|
||||
($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--image', 'bad image%name', '--dry-run');
|
||||
check($rc == 1, 'validation: image with whitespace or % refused');
|
||||
check_like($out, qr/Invalid --image/, 'validation: image message');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_api_key_argument {
|
||||
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 ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3', '--api-key', 'chosen-key-1234');
|
||||
check($rc == 0, 'api key argument: exit 0');
|
||||
check_like(slurp_file($ENVFILE), qr/^SGLANG_API_KEY=chosen-key-1234$/, 'api key argument: the key is stored');
|
||||
check_unlike($out, qr/chosen-key-1234/, 'api key argument: a given key is never echoed');
|
||||
my $unit = slurp_file($UNIT);
|
||||
check_unlike($unit, qr/chosen-key-1234/, 'api key argument: the key is not in the unit');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_non_root {
|
||||
reset_fixture();
|
||||
my $out_file = "$WORK/log/run.out";
|
||||
my $pid = fork();
|
||||
die "cannot fork: $!\n" unless defined $pid;
|
||||
if ($pid == 0) {
|
||||
open(STDOUT, '>', $out_file);
|
||||
open(STDERR, '>&', \*STDOUT);
|
||||
$ENV{PATH} = "$BIN:/usr/bin:/bin";
|
||||
$ENV{STUB_WORK} = $WORK;
|
||||
$) = 1000;
|
||||
$> = 1000;
|
||||
exec '/usr/bin/perl', $SCRIPT, '--version';
|
||||
exit 126;
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
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');
|
||||
|
||||
my $pid3 = fork();
|
||||
die "cannot fork: $!\n" unless defined $pid3;
|
||||
if ($pid3 == 0) {
|
||||
open(STDOUT, '>', $out_file);
|
||||
open(STDERR, '>&', \*STDOUT);
|
||||
$ENV{PATH} = "$BIN:/usr/bin:/bin";
|
||||
$ENV{STUB_WORK} = $WORK;
|
||||
$) = 1000;
|
||||
$> = 1000;
|
||||
exec '/usr/bin/perl', $SCRIPT, '--model', 'ZhipuAI/GLM-5.3';
|
||||
exit 126;
|
||||
}
|
||||
waitpid($pid3, 0);
|
||||
my $rc3 = $? >> 8;
|
||||
my $out3 = slurp_file($out_file);
|
||||
check($rc3 == 1, 'non-root: exit 1');
|
||||
check_like($out3, qr/must be run as root/, 'non-root: the message says so');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_offline {
|
||||
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('image-missing', "1\n");
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check($rc == 1, 'unpublished tag: exit 1');
|
||||
check_like($out, qr/resolved image tag does not exist/, 'unpublished tag: named as missing');
|
||||
check_like($out, qr/--image/, 'unpublished tag: names the way forward');
|
||||
return;
|
||||
}
|
||||
|
||||
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");
|
||||
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');
|
||||
|
||||
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', "\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');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_custom_layout {
|
||||
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 ($rc, $out) = run_script(
|
||||
'--model', 'ZhipuAI/GLM-5.3',
|
||||
'--service-name', 'llm',
|
||||
'--state-dir', '/srv/llm',
|
||||
'--cert-dir', '/etc/ssl/llm',
|
||||
'--port', '9000',
|
||||
'--tensor-parallel', '8',
|
||||
'--max-model-len', '131072',
|
||||
'--gpu-memory-utilization', '0.85',
|
||||
);
|
||||
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');
|
||||
# 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');
|
||||
check(-d '/srv/llm/modelscope', 'custom layout: the state directory follows');
|
||||
my $unit = slurp_file('/etc/systemd/system/llm.service');
|
||||
check_like($unit, qr/--port 9000/, 'custom layout: port in the unit');
|
||||
check_like($unit, qr/--tp-size 8/, 'custom layout: tensor parallel in the unit');
|
||||
check_like($unit, qr/--context-length 131072/, 'custom layout: context length in the unit');
|
||||
check_like($unit, qr/--mem-fraction-static 0\.85/, 'custom layout: memory fraction in the unit');
|
||||
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');
|
||||
remove_tree('/etc/ssl/llm');
|
||||
remove_tree('/srv/llm');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_tls_existing {
|
||||
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");
|
||||
run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
my $crt = slurp_file("$CERTDIR/sglang.crt");
|
||||
reset_log();
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check(slurp_file("$CERTDIR/sglang.crt") eq $crt, 'tls: an existing certificate is kept');
|
||||
check(count_in_log(qr/^openssl /) == 0, 'tls: no second certificate');
|
||||
check_like($out, qr/TLS certificate: configured/, 'tls: reported as configured');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_stale_container {
|
||||
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 $fh;
|
||||
open($fh, q{>}, "$ST/container.sglang") and close($fh);
|
||||
my ($rc, $out) = run_script('--uninstall');
|
||||
check($rc == 0, 'stale container: uninstall exit 0');
|
||||
check_like(stub_log(), qr/^podman rm -f sglang$/m, 'stale container: removed');
|
||||
check(!-e "$ST/container.sglang", 'stale container: gone from podman');
|
||||
return;
|
||||
}
|
||||
|
||||
sub scenario_selinux {
|
||||
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} = 'Permissive';
|
||||
my ($rc, $out) = run_script('--model', 'ZhipuAI/GLM-5.3');
|
||||
check(count_in_log(qr/^setsebool /) == 0, 'selinux: nothing set while permissive');
|
||||
check_like($out, qr/SELinux: permissive \(skipped\)/, 'selinux: reported as skipped');
|
||||
|
||||
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_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');
|
||||
delete $ENV{STUB_SELINUX};
|
||||
return;
|
||||
}
|
||||
|
||||
my %scenarios = (
|
||||
fresh => \&scenario_fresh,
|
||||
rerun => \&scenario_rerun,
|
||||
dry_run => \&scenario_dry_run,
|
||||
uninstall => \&scenario_uninstall,
|
||||
uninstall_twice => \&scenario_uninstall_twice,
|
||||
radeon => \&scenario_radeon,
|
||||
radeon_dev => \&scenario_radeon_dev,
|
||||
radeon_with_image => \&scenario_radeon_with_image,
|
||||
rocm_flavours => \&scenario_rocm_flavours,
|
||||
modelscope => \&scenario_modelscope,
|
||||
hardware => \&scenario_hardware,
|
||||
bad_os => \&scenario_bad_os,
|
||||
no_driver => \&scenario_no_driver,
|
||||
validation => \&scenario_validation,
|
||||
api_key_argument => \&scenario_api_key_argument,
|
||||
non_root => \&scenario_non_root,
|
||||
offline => \&scenario_offline,
|
||||
dependencies => \&scenario_dependency_section,
|
||||
custom_layout => \&scenario_custom_layout,
|
||||
tls_existing => \&scenario_tls_existing,
|
||||
stale_container => \&scenario_stale_container,
|
||||
selinux => \&scenario_selinux,
|
||||
);
|
||||
|
||||
prepare_stubs();
|
||||
prepare_devices();
|
||||
|
||||
my @wanted = @ARGV ? @ARGV : sort keys %scenarios;
|
||||
for my $name (@wanted) {
|
||||
if (!exists $scenarios{$name}) {
|
||||
print "FAIL unknown scenario $name\n";
|
||||
$failed++;
|
||||
next;
|
||||
}
|
||||
print "\n== $name ==\n";
|
||||
$scenarios{$name}->();
|
||||
}
|
||||
|
||||
print "\n" . ($failed ? "$failed FAILED of " . ($passed + $failed) : "all " . $passed . " checks passed") . "\n";
|
||||
exit($failed ? 1 : 0);
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/perl
|
||||
# Stub commands for the sglang-deploy.pl rig. One file, dispatched by $0's basename,
|
||||
# so every stubbed tool answers from the fixture under /rig/fixture and /rig/state.
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
|
||||
# Everything this stub reads and writes lives in the work directory the rig exports to
|
||||
# the script under test, which is why it is read from the environment: the stub is
|
||||
# reached through a link named after the command it stands in for.
|
||||
my $RIG = $ENV{STUB_WORK} // '/tmp/sglang-rig';
|
||||
my $FIX = "$RIG/fixture";
|
||||
my $ST = "$RIG/state";
|
||||
my $LOG = "$RIG/log/stubs.log";
|
||||
|
||||
my ($name) = $0 =~ m{([^/]+)$};
|
||||
my @args = @ARGV;
|
||||
|
||||
sub log_call {
|
||||
if (open(my $fh, '>>', $LOG)) {
|
||||
print {$fh} "$name @args\n";
|
||||
close($fh);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub fixture_text {
|
||||
my ($file, $default) = @_;
|
||||
my $path = "$FIX/$file";
|
||||
return $default unless -f $path;
|
||||
open(my $fh, '<', $path) or return $default;
|
||||
local $/ = undef;
|
||||
my $text = <$fh>;
|
||||
close($fh);
|
||||
return defined $text ? $text : $default;
|
||||
}
|
||||
|
||||
# Image names carry slashes and colons, which are not file names; the marker files keep
|
||||
# the image identity without them.
|
||||
sub state_name { my ($name) = @_; $name =~ s{[/:]}{_}g; return $name }
|
||||
sub state_exists { return -e "$ST/" . state_name($_[0]) ? 1 : 0 }
|
||||
sub touch_state {
|
||||
my $fh;
|
||||
open($fh, '>', "$ST/" . state_name($_[0])) and close($fh);
|
||||
return;
|
||||
}
|
||||
sub drop_state { unlink("$ST/" . state_name($_[0])); return }
|
||||
|
||||
log_call();
|
||||
|
||||
if ($name eq 'lspci') {
|
||||
print fixture_text('gpu.txt', '');
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'rpm') {
|
||||
my $joined = join(' ', @args);
|
||||
if ($joined =~ /%\{VERSION\}/ && $joined =~ /rocm-runtime/) {
|
||||
my $version = fixture_text('rocm.txt', '');
|
||||
$version =~ s/\s+$//;
|
||||
exit 1 unless length $version;
|
||||
print "$version\n";
|
||||
exit 0;
|
||||
}
|
||||
my $pkg = $args[-1];
|
||||
my $installed = fixture_text('rpm-installed', '');
|
||||
if ($installed =~ /^\Q$pkg\E$/m) {
|
||||
print "$pkg-1.0-1.x86_64\n";
|
||||
exit 0;
|
||||
}
|
||||
print STDERR "package $pkg is not installed\n";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
if ($name eq 'dnf') {
|
||||
my @pkgs = grep { !/^-/ && $_ ne 'install' } @args;
|
||||
my $installed = fixture_text('rpm-installed', '');
|
||||
for my $pkg (@pkgs) {
|
||||
$installed .= "$pkg\n" unless $installed =~ /^\Q$pkg\E$/m;
|
||||
}
|
||||
if (open(my $fh, '>', "$FIX/rpm-installed")) {
|
||||
print {$fh} $installed;
|
||||
close($fh);
|
||||
}
|
||||
print "Installing: @pkgs\n";
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'podman') {
|
||||
my $sub = $args[0] // '';
|
||||
if ($sub eq '--version') { print "podman version 5.8.4\n"; exit 0 }
|
||||
if ($sub eq 'pull') {
|
||||
my $image = $args[-1];
|
||||
touch_state("image.$image");
|
||||
print "Pulled $image\n";
|
||||
exit 0;
|
||||
}
|
||||
if ($sub eq 'image' && ($args[1] // '') eq 'exists') {
|
||||
exit(state_exists("image.$args[2]") ? 0 : 1);
|
||||
}
|
||||
if ($sub eq 'container' && ($args[1] // '') eq 'exists') {
|
||||
exit(state_exists("container.$args[2]") ? 0 : 1);
|
||||
}
|
||||
if ($sub eq 'rm') {
|
||||
my $target = $args[-1];
|
||||
drop_state("container.$target");
|
||||
print "$target\n";
|
||||
exit 0;
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'systemctl') {
|
||||
my @rest = grep { $_ !~ /^--/ } @args;
|
||||
my $verb = shift @rest // '';
|
||||
my $svc = $rest[0] // '';
|
||||
$svc =~ s/\.service$//;
|
||||
if ($verb eq 'is-active') { exit(state_exists("active.$svc") ? 0 : 1) }
|
||||
if ($verb eq 'is-enabled') { exit(state_exists("enabled.$svc") ? 0 : 1) }
|
||||
if ($verb eq 'start') { touch_state("active.$svc"); exit 0 }
|
||||
if ($verb eq 'stop') { drop_state("active.$svc"); exit 0 }
|
||||
if ($verb eq 'enable') { touch_state("enabled.$svc"); touch_state("active.$svc") if grep { $_ eq q{--now} } @args; exit 0 }
|
||||
if ($verb eq 'disable') { drop_state("enabled.$svc"); exit 0 }
|
||||
if ($verb eq 'try-restart') { exit(state_exists("active.$svc") ? 0 : 1) }
|
||||
if ($verb eq 'reload' || $verb eq 'daemon-reload') { exit 0 }
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'curl') {
|
||||
my ($url) = grep { /^https?:\/\// } @args;
|
||||
my $joined = join(' ', @args);
|
||||
$url //= '';
|
||||
if ($url =~ m{modelscope\.cn/api/v1/models/}) {
|
||||
my $code = fixture_text('ms.code', '200');
|
||||
$code =~ s/\s+$//;
|
||||
my $body = $joined =~ /__HTTP__/ ? fixture_text('ms.json', '{"Data":{"Name":"GLM-5.3"}}') : '';
|
||||
print $body;
|
||||
print "\n__HTTP__$code\n" if $joined =~ /__HTTP__/;
|
||||
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{hub\.docker\.com}) {
|
||||
if (-f "$FIX/image-missing") {
|
||||
print STDERR "curl: (22) The requested URL returned error: 404\n";
|
||||
exit 22;
|
||||
}
|
||||
if ($url =~ /tags\?/) {
|
||||
# A tag listing, newest first: the rig's AMD development build.
|
||||
print qq({"results":[{"name":"v0.5.19-rocm724-gfx1151-20260916"}]}\n);
|
||||
exit 0;
|
||||
}
|
||||
print qq({"name":"ok"}\n);
|
||||
exit 0;
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'openssl') {
|
||||
my %want;
|
||||
for my $i (0 .. $#args) {
|
||||
if ($args[$i] eq '-keyout') { $want{key} = $args[$i + 1] }
|
||||
if ($args[$i] eq '-out') { $want{crt} = $args[$i + 1] }
|
||||
if ($args[$i] eq '-subj') { $want{subj} = $args[$i + 1] }
|
||||
if ($args[$i] eq '-addext') { $want{san} = $args[$i + 1] }
|
||||
}
|
||||
for my $kind (qw(key crt)) {
|
||||
my $path = $want{$kind};
|
||||
next unless defined $path;
|
||||
if (open(my $fh, '>', $path)) {
|
||||
print {$fh} "stub $kind $want{subj} $want{san}\n";
|
||||
close($fh);
|
||||
}
|
||||
else {
|
||||
print STDERR "can't open $path\n";
|
||||
exit 1;
|
||||
}
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'nginx') {
|
||||
print "nginx: configuration file /etc/nginx/nginx.conf test is successful\n";
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'firewall-cmd') {
|
||||
my $joined = join(' ', @args);
|
||||
if ($joined =~ /--list-services/) {
|
||||
print fixture_text('fw-services', "\n");
|
||||
exit 0;
|
||||
}
|
||||
if ($joined =~ /--add-service=https/) {
|
||||
my $fh;
|
||||
open($fh, q{>>}, "$FIX/fw-services") and print {$fh} "https\n" and close($fh);
|
||||
exit 0;
|
||||
}
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'getenforce') {
|
||||
my $mode = $ENV{STUB_SELINUX} // 'Enforcing';
|
||||
print "$mode\n";
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'getsebool') {
|
||||
my $value = state_exists('sebool') ? 'on' : 'off';
|
||||
print "httpd_can_network_connect --> $value\n";
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($name eq 'setsebool') {
|
||||
touch_state('sebool');
|
||||
exit 0;
|
||||
}
|
||||
|
||||
exit 0;
|
||||
Reference in New Issue
Block a user