jina verze
This commit is contained in:
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
my ($file, $mode, @args) = @ARGV;
|
||||
|
||||
defined $file && length $file or die "usage: $0 <file> <mode> ...\n";
|
||||
defined $mode && length $mode or die "mode is required\n";
|
||||
|
||||
sub mask_to_re {
|
||||
my ($mask) = @_;
|
||||
return undef if !defined($mask) || $mask eq '';
|
||||
$mask = quotemeta($mask);
|
||||
$mask =~ s/\\\*/.*/g;
|
||||
return qr/\A$mask\z/;
|
||||
}
|
||||
|
||||
sub parse_kv_line {
|
||||
my ($line, $wanted_key) = @_;
|
||||
return unless $line =~ /^(\h*)\Q$wanted_key\E(?:\h+(.*?))?\h*(?:\R)?$/;
|
||||
my $indent = $1;
|
||||
my $value = defined($2) ? $2 : '';
|
||||
return ($indent, $value);
|
||||
}
|
||||
|
||||
sub line_matches_delete {
|
||||
my ($line, $wanted_key, $value_re) = @_;
|
||||
my @m = parse_kv_line($line, $wanted_key) or return 0;
|
||||
return 1 unless $value_re;
|
||||
return $m[1] =~ $value_re;
|
||||
}
|
||||
|
||||
sub line_matches_exact {
|
||||
my ($line, $wanted_key, $wanted_value) = @_;
|
||||
my @m = parse_kv_line($line, $wanted_key) or return 0;
|
||||
return $m[1] eq $wanted_value;
|
||||
}
|
||||
|
||||
sub fmt_line {
|
||||
my ($indent, $k, $v) = @_;
|
||||
return sprintf("%s%-23s %s\n", $indent, $k, $v);
|
||||
}
|
||||
|
||||
sub brace_delta {
|
||||
my ($line) = @_;
|
||||
my $o = () = $line =~ /\{/g;
|
||||
my $c = () = $line =~ /\}/g;
|
||||
return $o - $c;
|
||||
}
|
||||
|
||||
sub find_section {
|
||||
my ($lines, $re) = @_;
|
||||
|
||||
for (my $i = 0; $i <= $#$lines; $i++) {
|
||||
next unless $lines->[$i] =~ /^(\h*)$re\h*\{\h*(?:\R)?$/;
|
||||
|
||||
my $indent = $1 . ' ';
|
||||
my $depth = 1;
|
||||
|
||||
for (my $j = $i + 1; $j <= $#$lines; $j++) {
|
||||
$depth += brace_delta($lines->[$j]);
|
||||
if ($depth == 0) {
|
||||
return ($i, $j, $indent);
|
||||
}
|
||||
}
|
||||
|
||||
die "section '$re' is not closed\n";
|
||||
}
|
||||
|
||||
die "section '$re' not found\n";
|
||||
}
|
||||
|
||||
sub delete_key {
|
||||
my ($lines, $section_re, $key, $mask) = @_;
|
||||
my $value_re = mask_to_re($mask);
|
||||
|
||||
if ($section_re eq '') {
|
||||
my @out;
|
||||
my $depth = 0;
|
||||
|
||||
for my $line (@$lines) {
|
||||
my $remove = ($depth == 0 && line_matches_delete($line, $key, $value_re)) ? 1 : 0;
|
||||
push @out, $line unless $remove;
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
@$lines = @out;
|
||||
return;
|
||||
}
|
||||
|
||||
my ($start, $end, undef) = find_section($lines, $section_re);
|
||||
|
||||
my @out;
|
||||
push @out, @$lines[0 .. $start];
|
||||
|
||||
my $depth = 1;
|
||||
for my $i ($start + 1 .. $end - 1) {
|
||||
my $line = $lines->[$i];
|
||||
my $remove = ($depth == 1 && line_matches_delete($line, $key, $value_re)) ? 1 : 0;
|
||||
push @out, $line unless $remove;
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
push @out, @$lines[$end .. $#$lines];
|
||||
@$lines = @out;
|
||||
}
|
||||
|
||||
sub add_key {
|
||||
my ($lines, $section_re, $key, $value) = @_;
|
||||
die "value is required for add\n" if !defined($value) || $value eq '';
|
||||
|
||||
if ($section_re eq '') {
|
||||
my $depth = 0;
|
||||
|
||||
for my $line (@$lines) {
|
||||
if ($depth == 0 && line_matches_exact($line, $key, $value)) {
|
||||
return;
|
||||
}
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
my $new = fmt_line('', $key, $value);
|
||||
|
||||
my $root_pos;
|
||||
my $first_section_pos;
|
||||
$depth = 0;
|
||||
|
||||
for (my $i = 0; $i <= $#$lines; $i++) {
|
||||
my $line = $lines->[$i];
|
||||
|
||||
if ($depth == 0) {
|
||||
$root_pos = $i
|
||||
if !defined($root_pos) && $line =~ /^\h*serverName\h+\S*/;
|
||||
|
||||
$first_section_pos = $i
|
||||
if !defined($first_section_pos) && $line =~ /^\h*\S.*\{\h*(?:\R)?$/;
|
||||
}
|
||||
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
if (defined $root_pos) {
|
||||
splice @$lines, $root_pos + 1, 0, $new;
|
||||
} elsif (defined $first_section_pos) {
|
||||
splice @$lines, $first_section_pos, 0, $new;
|
||||
} else {
|
||||
push @$lines, $new;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
my ($start, $end, $indent) = find_section($lines, $section_re);
|
||||
|
||||
my $depth = 1;
|
||||
for my $i ($start + 1 .. $end - 1) {
|
||||
my $line = $lines->[$i];
|
||||
if ($depth == 1 && line_matches_exact($line, $key, $value)) {
|
||||
return;
|
||||
}
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
my $new = fmt_line($indent, $key, $value);
|
||||
splice @$lines, $end, 0, $new;
|
||||
}
|
||||
|
||||
sub build_vhost_section {
|
||||
my ($domain) = @_;
|
||||
return if !defined($domain) || $domain eq '';
|
||||
|
||||
my @out;
|
||||
push @out, "virtualhost $domain {\n";
|
||||
push @out, fmt_line(' ', 'vhRoot', "/var/www/vhosts/$domain");
|
||||
push @out, fmt_line(' ', 'configFile', "/usr/local/lsws/conf/vhosts/$domain.conf");
|
||||
push @out, fmt_line(' ', 'allowSymbolLink', '1');
|
||||
push @out, fmt_line(' ', 'enableScript', '1');
|
||||
push @out, fmt_line(' ', 'restrained', '1');
|
||||
push @out, fmt_line(' ', 'setUIDMode', '2');
|
||||
push @out, "}\n";
|
||||
|
||||
return join('', @out);
|
||||
}
|
||||
|
||||
sub has_vhost_section {
|
||||
my ($lines, $name) = @_;
|
||||
|
||||
for my $line (@$lines) {
|
||||
return 1 if $line =~ /^\h*virtualhost\h+\Q$name\E\h*\{/;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub add_vhost_section {
|
||||
my ($lines, $name) = @_;
|
||||
|
||||
return if !defined($name) || $name eq '';
|
||||
return if has_vhost_section($lines, $name);
|
||||
|
||||
my @section = split /(?<=\n)/, build_vhost_section($name), -1;
|
||||
pop @section if @section && $section[-1] eq '';
|
||||
|
||||
if (@$lines && $lines->[-1] !~ /\n\z/) {
|
||||
$lines->[-1] .= "\n";
|
||||
}
|
||||
|
||||
push @$lines, "\n" if @$lines && $lines->[-1] !~ /^\s*$/;
|
||||
push @$lines, @section;
|
||||
}
|
||||
|
||||
sub delete_named_section {
|
||||
my ($lines, $header_re) = @_;
|
||||
|
||||
my @out;
|
||||
my $deleted = 0;
|
||||
|
||||
for (my $i = 0; $i <= $#$lines; $i++) {
|
||||
my $line = $lines->[$i];
|
||||
|
||||
if ($line =~ /^\h*$header_re\h*\{\h*(?:\R)?$/) {
|
||||
my $depth = 1;
|
||||
$deleted = 1;
|
||||
|
||||
for ($i = $i + 1; $i <= $#$lines; $i++) {
|
||||
$depth += brace_delta($lines->[$i]);
|
||||
last if $depth == 0;
|
||||
}
|
||||
|
||||
next;
|
||||
}
|
||||
|
||||
push @out, $line;
|
||||
}
|
||||
|
||||
@$lines = @out;
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
sub get_suspended_vhosts {
|
||||
my ($lines) = @_;
|
||||
|
||||
my $depth = 0;
|
||||
for my $line (@$lines) {
|
||||
if ($depth == 0) {
|
||||
my @m = parse_kv_line($line, 'suspendedVhosts');
|
||||
if (@m) {
|
||||
return grep { length }
|
||||
map { s/^\h+|\h+$//gr }
|
||||
split /,/, $m[1];
|
||||
}
|
||||
}
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub set_suspended_vhosts {
|
||||
my ($lines, @list) = @_;
|
||||
|
||||
@list = grep { defined($_) && $_ ne '' } @list;
|
||||
|
||||
delete_key($lines, '', 'suspendedVhosts', '');
|
||||
|
||||
if (@list) {
|
||||
add_key($lines, '', 'suspendedVhosts', join(',', @list));
|
||||
}
|
||||
}
|
||||
|
||||
sub suspend_vhost {
|
||||
my ($lines, $domain) = @_;
|
||||
return if !defined($domain) || $domain eq '';
|
||||
|
||||
my @current = get_suspended_vhosts($lines);
|
||||
|
||||
my @new = ($domain);
|
||||
for my $item (@current) {
|
||||
push @new, $item if $item ne $domain;
|
||||
}
|
||||
|
||||
set_suspended_vhosts($lines, @new);
|
||||
}
|
||||
|
||||
sub unsuspend_vhost {
|
||||
my ($lines, $domain) = @_;
|
||||
return if !defined($domain) || $domain eq '';
|
||||
|
||||
my @current = get_suspended_vhosts($lines);
|
||||
my @new = grep { $_ ne $domain } @current;
|
||||
|
||||
set_suspended_vhosts($lines, @new);
|
||||
}
|
||||
|
||||
sub get_listener_http_maps {
|
||||
my ($lines) = @_;
|
||||
|
||||
my ($start, $end, undef) = find_section($lines, 'listener\h+HTTP');
|
||||
|
||||
my @maps;
|
||||
|
||||
for my $i ($start + 1 .. $end - 1) {
|
||||
my $clean = $lines->[$i];
|
||||
$clean =~ s/#.*$//;
|
||||
$clean =~ s/^\s+|\s+$//g;
|
||||
next if $clean eq '';
|
||||
|
||||
if ($clean =~ /^map\s+(\S+)\s+(\S+)$/i) {
|
||||
push @maps, [$1, $2];
|
||||
}
|
||||
}
|
||||
|
||||
return @maps;
|
||||
}
|
||||
|
||||
sub vhost_list {
|
||||
my ($lines, $type) = @_;
|
||||
$type //= 'all';
|
||||
|
||||
die "unknown vhost list type '$type'\n"
|
||||
if $type !~ /\A(?:all|up|down)\z/;
|
||||
|
||||
my @all;
|
||||
my %down = map { $_ => 1 } get_suspended_vhosts($lines);
|
||||
|
||||
for my $line (@$lines) {
|
||||
my $clean = $line;
|
||||
$clean =~ s/#.*$//;
|
||||
$clean =~ s/^\s+|\s+$//g;
|
||||
next if $clean eq '';
|
||||
|
||||
if ($clean =~ /^virtualhost\s+([^\s\{]+)\s*\{?/i) {
|
||||
push @all, $1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($type eq 'all') {
|
||||
print "$_\n" for @all;
|
||||
} elsif ($type eq 'down') {
|
||||
print "$_\n" for grep { exists $down{$_} } @all;
|
||||
} elsif ($type eq 'up') {
|
||||
print "$_\n" for grep { !exists $down{$_} } @all;
|
||||
}
|
||||
}
|
||||
|
||||
sub vhost_list_map {
|
||||
my ($lines, $type) = @_;
|
||||
$type //= 'all';
|
||||
|
||||
die "unknown vhost map list type '$type'\n"
|
||||
if $type !~ /\A(?:all|up|down)\z/;
|
||||
|
||||
my %down = map { $_ => 1 } get_suspended_vhosts($lines);
|
||||
my %seen;
|
||||
|
||||
for my $map (get_listener_http_maps($lines)) {
|
||||
my ($vhost, $domain) = @$map;
|
||||
|
||||
next if $vhost ne $domain;
|
||||
next if $seen{$vhost}++;
|
||||
|
||||
if ($type eq 'down') {
|
||||
next if !exists $down{$vhost};
|
||||
} elsif ($type eq 'up') {
|
||||
next if exists $down{$vhost};
|
||||
}
|
||||
|
||||
print "$vhost\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub alias_list {
|
||||
my ($lines) = @_;
|
||||
|
||||
my %seen;
|
||||
|
||||
for my $map (get_listener_http_maps($lines)) {
|
||||
my ($vhost, $domain) = @$map;
|
||||
|
||||
next if $vhost eq $domain;
|
||||
next if $seen{$domain}++;
|
||||
|
||||
print "$domain\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub vhost_alias {
|
||||
my ($lines, $name) = @_;
|
||||
|
||||
die "virtual host name is required\n"
|
||||
if !defined($name) || $name eq '';
|
||||
|
||||
my %seen;
|
||||
|
||||
for my $map (get_listener_http_maps($lines)) {
|
||||
my ($vhost, $domain) = @$map;
|
||||
|
||||
next if $vhost ne $name;
|
||||
next if $domain eq $name;
|
||||
next if $seen{$domain}++;
|
||||
|
||||
print "$domain\n";
|
||||
}
|
||||
}
|
||||
|
||||
open my $fh, '<', $file or die "cannot read '$file': $!\n";
|
||||
local $/;
|
||||
my $content = <$fh>;
|
||||
close $fh;
|
||||
|
||||
my @L = split /(?<=\n)/, $content, -1;
|
||||
|
||||
if ($mode eq 'del') {
|
||||
my ($section_re, $key, $mask) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
$mask //= '';
|
||||
delete_key(\@L, $section_re, $key, $mask);
|
||||
}
|
||||
elsif ($mode eq 'add') {
|
||||
my ($section_re, $key, $value) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $value or die "value is required\n";
|
||||
add_key(\@L, $section_re, $key, $value);
|
||||
}
|
||||
elsif ($mode eq 'set') {
|
||||
my ($section_re, $key, $value) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $value or die "value is required\n";
|
||||
delete_key(\@L, $section_re, $key, '');
|
||||
add_key(\@L, $section_re, $key, $value);
|
||||
}
|
||||
elsif ($mode eq 'set_masked') {
|
||||
my ($section_re, $key, $mask, $value) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $mask or die "mask is required\n";
|
||||
defined $value or die "value is required\n";
|
||||
delete_key(\@L, $section_re, $key, $mask);
|
||||
add_key(\@L, $section_re, $key, $value);
|
||||
}
|
||||
elsif ($mode eq 'del_masked') {
|
||||
my ($section_re, $key, $mask) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $mask or die "mask is required\n";
|
||||
delete_key(\@L, $section_re, $key, $mask);
|
||||
}
|
||||
elsif ($mode eq 'vhost_add') {
|
||||
my ($name) = @args;
|
||||
defined $name && length $name or die "virtual host name is required\n";
|
||||
add_vhost_section(\@L, $name);
|
||||
}
|
||||
elsif ($mode eq 'vhost_del') {
|
||||
my ($name) = @args;
|
||||
defined $name && length $name or die "virtual host name is required\n";
|
||||
my $quoted = quotemeta($name);
|
||||
delete_named_section(\@L, "virtualhost\\h+$quoted");
|
||||
}
|
||||
elsif ($mode eq 'vhost_down') {
|
||||
my ($name) = @args;
|
||||
defined $name && length $name or die "virtual host name is required\n";
|
||||
suspend_vhost(\@L, $name);
|
||||
}
|
||||
elsif ($mode eq 'vhost_up') {
|
||||
my ($name) = @args;
|
||||
defined $name && length $name or die "virtual host name is required\n";
|
||||
unsuspend_vhost(\@L, $name);
|
||||
}
|
||||
elsif ($mode eq 'vhost_list') {
|
||||
my ($type) = @args;
|
||||
vhost_list(\@L, $type // 'all');
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'vhost_list_map') {
|
||||
my ($type) = @args;
|
||||
vhost_list_map(\@L, $type // 'all');
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'alias_list') {
|
||||
alias_list(\@L);
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'vhost_alias') {
|
||||
my ($name) = @args;
|
||||
vhost_alias(\@L, $name);
|
||||
exit 0;
|
||||
}
|
||||
else {
|
||||
die "unknown mode '$mode'\n";
|
||||
}
|
||||
|
||||
$content = join '', @L;
|
||||
|
||||
open my $out, '>', $file or die "cannot write '$file': $!\n";
|
||||
print {$out} $content;
|
||||
close $out or die "cannot close '$file': $!\n";
|
||||
|
||||
exit 0;
|
||||
@@ -0,0 +1,134 @@
|
||||
backupDailyDirPattern="^20[0-9]{2}-[0-9]{2}-[0-9]{2}$backupDailySuffix$"
|
||||
backupArchiveDirPattern="^20[0-9]{2}-[0-9]{2}-[0-9]{2}$backupArchiveSuffix$"
|
||||
|
||||
# Generates a backup destination path; uses $backupFirst for the first backup of the day, $appTime otherwise.
|
||||
# [$1] (path): explicit destination path; returned unchanged if provided.
|
||||
function backupPathGenerate() {
|
||||
local path="$1"
|
||||
|
||||
if [[ -z "$path" ]]; then
|
||||
path="$backupDailyPath/$appDate/$backupFirstDir"
|
||||
[[ -d "$path" ]] && path="$backupDailyPath/$appDate/$appTime"
|
||||
fi
|
||||
|
||||
printf '%s' "$path"
|
||||
}
|
||||
|
||||
# Creates a file backup archive for a site.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (file): target archive file path.
|
||||
function backupSiteFiles() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local file="$2"
|
||||
[[ -n "$file" ]] || { appError "Target file not specified"; return 1; }
|
||||
|
||||
local compressProgram=""
|
||||
if [[ -n "${pigzThreads:-}" ]] && command -v pigz &>/dev/null; then
|
||||
compressProgram="pigz -p $pigzThreads"
|
||||
fi
|
||||
|
||||
archiveCreate "$vhostsPath/$domain" "$file" "$compressProgram"
|
||||
}
|
||||
|
||||
# Creates a database backup for a site.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (file): target database backup file path.
|
||||
function backupSiteDatabase() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local file="$2"
|
||||
[[ -n "$file" ]] || { appError "Target file not specified"; return 1; }
|
||||
|
||||
local databaseName databaseUser databasePass
|
||||
databaseName=$(siteConfigGet "$domain" "databaseName")
|
||||
[[ -n "$databaseName" ]] || { appError "databaseName not found or empty"; return 1; }
|
||||
databaseUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
[[ -n "$databaseUser" ]] || { appError "databaseUser not found or empty"; return 1; }
|
||||
databasePass=$(siteConfigGet "$domain" "databasePass")
|
||||
[[ -n "$databasePass" ]] || { appError "databasePass not found or empty"; return 1; }
|
||||
|
||||
mariadbMasterExport "$databaseUser" "$databasePass" "$databaseName" "$file"
|
||||
}
|
||||
|
||||
# Creates a full backup for a site: files, database, and OLS vhost config.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (path): destination directory; auto-generated if omitted.
|
||||
# [$3] (type): backup type: zip, tar, or archive (defaults to $backupType).
|
||||
function backupSite() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local path
|
||||
path=$(backupPathGenerate "$2")
|
||||
|
||||
local type="${3:-$backupType}"
|
||||
case "$type" in
|
||||
zip)
|
||||
backupSiteFiles "$domain" "$path/$domain.zip" || return 1
|
||||
backupSiteDatabase "$domain" "$path/$domain.sql.zip" || return 1
|
||||
;;
|
||||
tar|archive)
|
||||
backupSiteFiles "$domain" "$path/$domain.tar.gz" || return 1
|
||||
backupSiteDatabase "$domain" "$path/$domain.sql.tar.gz" || return 1
|
||||
;;
|
||||
*)
|
||||
appError "Unknown type: $type"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
cp -f -- "$openlitespeedVhostsPath/$domain.conf" "$path/$domain.conf" || {
|
||||
appError "Failed to copy vhost config: $domain"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Syncs a local path to a remote server using rsync.
|
||||
# $1 (sourcePath): local path to sync.
|
||||
function storagePathSyncRsync() {
|
||||
local sourcePath="$1"
|
||||
[[ -n "$sourcePath" ]] || { appError "Source path not specified"; return 1; }
|
||||
|
||||
rsync -aH --delete-after --partial --partial-dir=.rsync-partial --password-file="$rsyncPassFile" "$sourcePath" rsync://"$rsyncUri$rsyncPath"
|
||||
}
|
||||
|
||||
# Syncs a local path to a remote server over SSH using rsync.
|
||||
# $1 (sourcePath): local path to sync.
|
||||
function storagePathSyncSSH() {
|
||||
local sourcePath="$1"
|
||||
[[ -n "$sourcePath" ]] || { appError "Source path not specified"; return 1; }
|
||||
|
||||
rsync -aH --delete-after --partial --partial-dir=.rsync-partial -e "ssh -i $sshKeyFile" "$sourcePath" "$sshUser@$sshHost:$sshPath"
|
||||
}
|
||||
|
||||
function storageBackupRemove() {
|
||||
local backup error
|
||||
backup="$1"
|
||||
[[ -n "$backup" ]] || { appError "Backup not specified"; return 1; }
|
||||
|
||||
case "$storageType" in
|
||||
rsync)
|
||||
if ! runError error rsyncDirectoryClean "$rsyncPath/$backup"; then
|
||||
appError "$error"
|
||||
elif ! runError error ftpDirectoryDelete "/$backup"; then
|
||||
appError "$error"
|
||||
fi
|
||||
;;
|
||||
ssh)
|
||||
if ! runError error sshDirectoryDelete "/$backup"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
appError "Unknown storageType: $storageType"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,931 @@
|
||||
#!/bin/bash
|
||||
|
||||
diagLabel="[Diag]"
|
||||
diagExcludeVhostsRegex='localhost|Example'
|
||||
|
||||
# Writes a section header to the diag log file and prints it to the info log.
|
||||
# $1 (title): section title text.
|
||||
function diagLogSection() {
|
||||
local title="$1"
|
||||
local padding
|
||||
local logPath
|
||||
|
||||
logInfo "$diagLabel $title"
|
||||
|
||||
padding=$((72 - ${#title}))
|
||||
if (( padding > 0 )); then
|
||||
title="${title} $(printf '.%.s' $(seq 1 "$padding"))"
|
||||
fi
|
||||
|
||||
logPath=$(dirname "$diagFile")
|
||||
[[ -d "$logPath" ]] || mkdir -p "$logPath"
|
||||
|
||||
printf "[ %s ]\n" "$title" >> "$diagFile"
|
||||
}
|
||||
|
||||
# Runs a command and appends its output (stdout and stderr) to the diag file.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function diagCmd() {
|
||||
echo "+ $*" >> "$diagFile"
|
||||
"$@" >> "$diagFile" 2>&1 || true
|
||||
echo >> "$diagFile"
|
||||
}
|
||||
|
||||
# Runs a shell command via bash -lc and appends its output to the diag file; errors ignored.
|
||||
# $@ (...): shell command string to execute.
|
||||
function diagShTry() {
|
||||
echo "+ $*" >> "$diagFile"
|
||||
bash -lc "$*" >> "$diagFile" 2>&1 || true
|
||||
echo >> "$diagFile"
|
||||
}
|
||||
|
||||
# Runs a kubectl command via k3sRun and appends its output to the diag file; errors ignored.
|
||||
# $@ (...): kubectl arguments to pass to k3sRun.
|
||||
function diagK3sTry() {
|
||||
echo "+ k3sRun $*" >> "$diagFile"
|
||||
k3sRun "$@" >> "$diagFile" 2>&1 || true
|
||||
echo >> "$diagFile"
|
||||
}
|
||||
|
||||
# Converts $diagSince (e.g. 4h, 30m, 2d) to a human-readable date argument for the date command.
|
||||
function diagSinceDateArg() {
|
||||
local s="${diagSince:-4h}"
|
||||
|
||||
case "$s" in
|
||||
*m)
|
||||
printf '%s minutes ago' "${s%m}"
|
||||
;;
|
||||
*h)
|
||||
printf '%s hours ago' "${s%h}"
|
||||
;;
|
||||
*d)
|
||||
printf '%s days ago' "${s%d}"
|
||||
;;
|
||||
*)
|
||||
printf '4 hours ago'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Returns the Unix epoch timestamp corresponding to the $diagSince cutoff time.
|
||||
function diagCutoffEpoch() {
|
||||
date -d "$(diagSinceDateArg)" +%s 2>/dev/null || date -d "4 hours ago" +%s
|
||||
}
|
||||
|
||||
# Returns the HH:MM:SS start time for sar based on $diagSince; falls back to 00:00:00 if in the past.
|
||||
function diagSarStartTime() {
|
||||
local t now
|
||||
|
||||
t="$(date -d "$(diagSinceDateArg)" +%H:%M:%S 2>/dev/null || echo "00:00:00")"
|
||||
now="$(date +%H:%M:%S)"
|
||||
|
||||
if [[ "$t" > "$now" ]]; then
|
||||
printf "00:00:00"
|
||||
else
|
||||
printf "%s" "$t"
|
||||
fi
|
||||
}
|
||||
|
||||
# Writes diagnostic metadata (timestamp, since, hostname, file path) to the diag file.
|
||||
function diagMeta() {
|
||||
diagLogSection "Diagnostic metadata"
|
||||
|
||||
{
|
||||
echo "started_at: $(date -Iseconds)"
|
||||
echo "since: $diagSince"
|
||||
echo "hostname: [SERVER]"
|
||||
echo "file: $diagFile"
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Replaces hostnames, user paths, and UIDs in the diag file with redacted placeholders.
|
||||
function diagSanitizeReport() {
|
||||
local hostLower
|
||||
local hostFqdn
|
||||
local hostFqdnLower
|
||||
local sanitizeHosts
|
||||
|
||||
[[ -f "$diagFile" ]] || return 0
|
||||
|
||||
hostLower="$(printf '%s' "$hostName" | tr '[:upper:]' '[:lower:]')"
|
||||
hostFqdn="$(hostname -f 2>/dev/null || true)"
|
||||
hostFqdnLower="$(printf '%s' "$hostFqdn" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
sanitizeHosts="$(
|
||||
printf '%s\n' "$hostName" "$hostLower" "$hostFqdn" "$hostFqdnLower" | awk 'NF && !seen[$0]++'
|
||||
)"
|
||||
|
||||
SANITIZE_HOSTS="$sanitizeHosts" perl -0pi -e '
|
||||
my $hosts = $ENV{SANITIZE_HOSTS} // "";
|
||||
for my $h (split /\n/, $hosts) {
|
||||
next unless defined $h && length $h;
|
||||
my $q = quotemeta($h);
|
||||
s/$q/[SERVER]/g;
|
||||
}
|
||||
|
||||
s#(/media/)[^/\s]+#$1[USER]#g;
|
||||
s#(/run/user/)[0-9]+#$1[UID]#g;
|
||||
' "$diagFile"
|
||||
}
|
||||
|
||||
# Filters stdin lines, excluding those matching reserved vhost names ($diagExcludeVhostsRegex).
|
||||
function diagGrepExcludeReserved() {
|
||||
local regex="${diagExcludeVhostsRegex:-}"
|
||||
|
||||
if [[ -n "$regex" ]]; then
|
||||
grep -Eiv "$regex"
|
||||
else
|
||||
cat
|
||||
fi
|
||||
}
|
||||
|
||||
# Appends filtered disk usage (df -h, excluding overlay/tmpfs/pod mounts) to the diag file.
|
||||
function diagDfUsage() {
|
||||
{
|
||||
echo "+ df -h filtered"
|
||||
df -h \
|
||||
-x tmpfs \
|
||||
-x devtmpfs \
|
||||
-x overlay \
|
||||
-x squashfs \
|
||||
-x efivarfs \
|
||||
2>/dev/null | awk '
|
||||
NR == 1 { print; next }
|
||||
$6 ~ "^/run/k3s/" { next }
|
||||
$6 ~ "^/var/lib/kubelet/pods/" { next }
|
||||
$6 ~ "^/run/user/" { next }
|
||||
$6 ~ "^/media/" { next }
|
||||
{ print }
|
||||
'
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends filtered inode usage (df -i, excluding overlay/tmpfs/pod mounts) to the diag file.
|
||||
function diagDfInodes() {
|
||||
{
|
||||
echo "+ df -i filtered"
|
||||
df -i \
|
||||
-x tmpfs \
|
||||
-x devtmpfs \
|
||||
-x overlay \
|
||||
-x squashfs \
|
||||
-x efivarfs \
|
||||
2>/dev/null | awk '
|
||||
NR == 1 { print; next }
|
||||
$6 ~ "^/run/k3s/" { next }
|
||||
$6 ~ "^/var/lib/kubelet/pods/" { next }
|
||||
$6 ~ "^/run/user/" { next }
|
||||
$6 ~ "^/media/" { next }
|
||||
{ print }
|
||||
'
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends iostat extended stats (1s interval, 5 samples, loop/sr devices excluded) to the diag file.
|
||||
function diagIostat() {
|
||||
{
|
||||
echo "+ iostat -x -z 1 5 | filter loop/sr devices"
|
||||
|
||||
{
|
||||
iostat -x -z 1 5 2>/dev/null || iostat -x 1 5 2>/dev/null
|
||||
} | awk '
|
||||
/^loop[0-9]+/ { next }
|
||||
/^sr[0-9]+/ { next }
|
||||
{ print }
|
||||
'
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Runs a sar command and appends its output to the diag file; skips gracefully if data is unavailable.
|
||||
# $1 (title): section title for the diag log.
|
||||
# $2 (cmd): sar command string to execute.
|
||||
function diagSarTry() {
|
||||
local title="$1"
|
||||
local cmd="$2"
|
||||
local saFile
|
||||
|
||||
saFile="/var/log/sysstat/sa$(date +%d)"
|
||||
|
||||
diagLogSection "$title"
|
||||
|
||||
{
|
||||
echo "+ $cmd"
|
||||
|
||||
if [[ ! -r "$saFile" ]]; then
|
||||
echo "sar_data_available: no"
|
||||
echo "sar_file: $saFile"
|
||||
echo "hint: enable sysstat data collection if SAR trends are needed"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
bash -lc "$cmd" || true
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends a filtered k3s node summary (conditions, capacity, allocated resources) to the diag file.
|
||||
function diagK3sNodeSummary() {
|
||||
{
|
||||
echo "+ k3sRun describe node filtered"
|
||||
|
||||
echo
|
||||
echo "Conditions:"
|
||||
k3sRun describe node 2>/dev/null | awk '
|
||||
/^Conditions:/ { p=1; next }
|
||||
/^Addresses:/ { p=0 }
|
||||
p {
|
||||
if ($1 == "Type" ||
|
||||
$1 == "----" ||
|
||||
$1 == "MemoryPressure" ||
|
||||
$1 == "DiskPressure" ||
|
||||
$1 == "PIDPressure" ||
|
||||
$1 == "Ready") {
|
||||
print
|
||||
}
|
||||
}
|
||||
' || true
|
||||
|
||||
echo
|
||||
echo "Capacity/Allocatable:"
|
||||
k3sRun describe node 2>/dev/null | awk '
|
||||
/^Capacity:/ { p=1; print; next }
|
||||
/^System Info:/ { p=0 }
|
||||
p && $1 !~ /InternalIP|Hostname|Machine|UUID|Boot|ProviderID/ { print }
|
||||
' || true
|
||||
|
||||
echo
|
||||
echo "Allocated resources:"
|
||||
k3sRun describe node 2>/dev/null | awk '
|
||||
/^Allocated resources:/ { p=1; print; next }
|
||||
/^Events:/ { p=0 }
|
||||
p { print }
|
||||
' || true
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends a MariaDB slave replication status summary to the diag file.
|
||||
function diagMariaDBReplicaSummary() {
|
||||
local tmp
|
||||
|
||||
tmp="$(mktemp)"
|
||||
|
||||
diagLogSection "MariaDB slave replication summary"
|
||||
|
||||
{
|
||||
echo "+ mariadbSlaveRootQuery SHOW REPLICA STATUS"
|
||||
echo
|
||||
|
||||
mariadbSlaveRootQuery "SHOW REPLICA STATUS\G" > "$tmp" 2>&1 || true
|
||||
|
||||
if grep -q 'Slave_IO_Running:' "$tmp"; then
|
||||
awk -F': ' '
|
||||
/^[[:space:]]*Slave_IO_State:/ { print "Slave_IO_State:", $2 }
|
||||
/^[[:space:]]*Slave_IO_Running:/ { print "Slave_IO_Running:", $2 }
|
||||
/^[[:space:]]*Slave_SQL_Running:/ { print "Slave_SQL_Running:", $2 }
|
||||
/^[[:space:]]*Seconds_Behind_Master:/ { print "Seconds_Behind_Master:", $2 }
|
||||
/^[[:space:]]*Last_IO_Errno:/ { print "Last_IO_Errno:", $2 }
|
||||
/^[[:space:]]*Last_SQL_Errno:/ { print "Last_SQL_Errno:", $2 }
|
||||
/^[[:space:]]*Relay_Log_Space:/ { print "Relay_Log_Space:", $2 }
|
||||
/^[[:space:]]*Read_Master_Log_Pos:/ { print "Read_Master_Log_Pos:", $2 }
|
||||
/^[[:space:]]*Exec_Master_Log_Pos:/ { print "Exec_Master_Log_Pos:", $2 }
|
||||
/^[[:space:]]*Using_Gtid:/ { print "Using_Gtid:", $2 }
|
||||
/^[[:space:]]*Parallel_Mode:/ { print "Parallel_Mode:", $2 }
|
||||
/^[[:space:]]*Slave_SQL_Running_State:/ { print "Slave_SQL_Running_State:", $2 }
|
||||
' "$tmp" || true
|
||||
|
||||
echo
|
||||
rm -f "$tmp"
|
||||
return
|
||||
fi
|
||||
|
||||
: > "$tmp"
|
||||
mariadbSlaveRootQuery "SHOW REPLICA STATUS;" > "$tmp" 2>&1 || true
|
||||
|
||||
if [[ ! -s "$tmp" ]]; then
|
||||
echo "replication_status_available: no"
|
||||
echo
|
||||
rm -f "$tmp"
|
||||
return
|
||||
fi
|
||||
|
||||
awk -F'\t' '
|
||||
NF >= 12 {
|
||||
print "Slave_IO_State:", $1
|
||||
print "Master_Host: [REDACTED]"
|
||||
print "Master_User: [REDACTED]"
|
||||
print "Master_Port:", $4
|
||||
print "Master_Log_File:", $6
|
||||
print "Read_Master_Log_Pos:", $7
|
||||
print "Relay_Log_File:", $8
|
||||
print "Relay_Log_Pos:", $9
|
||||
print "Relay_Master_Log_File:", $10
|
||||
print "Slave_IO_Running:", $11
|
||||
print "Slave_SQL_Running:", $12
|
||||
found=1
|
||||
next
|
||||
}
|
||||
{
|
||||
print
|
||||
}
|
||||
END {
|
||||
if (!found) {
|
||||
print "replication_status_parse: raw fallback"
|
||||
}
|
||||
}
|
||||
' "$tmp" || true
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
# Parses the MariaDB slow query log since $diagSince and appends a fingerprint summary to the diag file.
|
||||
function diagMariaDBSlowSummary() {
|
||||
local mariadbMasterSlowLog="$mariadbMasterPath/slow.log"
|
||||
local cutoff
|
||||
|
||||
cutoff="$(diagCutoffEpoch)"
|
||||
|
||||
diagLogSection "MariaDB slow query summary only"
|
||||
|
||||
{
|
||||
echo "+ summarize slow log: $mariadbMasterSlowLog since ${diagSince:-4h}"
|
||||
echo
|
||||
|
||||
if [[ ! -r "$mariadbMasterSlowLog" ]]; then
|
||||
echo "slow_log_readable: no"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
perl -MTime::Local - "$mariadbMasterSlowLog" "$cutoff" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
use Time::Local;
|
||||
|
||||
my ($file, $cutoff) = @ARGV;
|
||||
|
||||
open my $fh, '<', $file or do {
|
||||
print "slow_log_readable: no\n";
|
||||
exit 0;
|
||||
};
|
||||
|
||||
my %fp;
|
||||
my $total = 0;
|
||||
my $max_qt = 0;
|
||||
my $max_rows_examined = 0;
|
||||
my $max_rows_sent = 0;
|
||||
|
||||
my ($active, $ts, $qt, $rows_sent, $rows_examined, $sql);
|
||||
|
||||
sub reset_entry {
|
||||
$active = 0;
|
||||
$ts = 0;
|
||||
$qt = 0;
|
||||
$rows_sent = 0;
|
||||
$rows_examined = 0;
|
||||
$sql = '';
|
||||
}
|
||||
|
||||
sub fingerprint_sql {
|
||||
my ($s) = @_;
|
||||
|
||||
$s =~ s/`[^`]*`/`X`/g;
|
||||
$s =~ s/'(?:\\.|[^'\\])*'/'X'/g;
|
||||
$s =~ s/"(?:\\.|[^"\\])*"/"X"/g;
|
||||
$s =~ s/\b[0-9a-fA-F]{16,}\b/HEX/g;
|
||||
$s =~ s/\b\d+(?:\.\d+)?\b/N/g;
|
||||
$s =~ s/\s+/ /g;
|
||||
$s =~ s/^\s+|\s+$//g;
|
||||
|
||||
return substr($s, 0, 220);
|
||||
}
|
||||
|
||||
sub flush_entry {
|
||||
return unless $active;
|
||||
return if $ts < $cutoff;
|
||||
|
||||
$total++;
|
||||
$max_qt = $qt if $qt > $max_qt;
|
||||
$max_rows_examined = $rows_examined if $rows_examined > $max_rows_examined;
|
||||
$max_rows_sent = $rows_sent if $rows_sent > $max_rows_sent;
|
||||
|
||||
my $f = fingerprint_sql($sql);
|
||||
return unless length $f;
|
||||
|
||||
$fp{$f}{count}++;
|
||||
$fp{$f}{total_time} += $qt;
|
||||
$fp{$f}{total_rows_examined} += $rows_examined;
|
||||
$fp{$f}{max_time} = $qt if !defined($fp{$f}{max_time}) || $qt > $fp{$f}{max_time};
|
||||
}
|
||||
|
||||
reset_entry();
|
||||
|
||||
while (my $line = <$fh>) {
|
||||
chomp $line;
|
||||
|
||||
if ($line =~ /^# Time:\s*(\d{2})(\d{2})(\d{2})\s+(\d{1,2}):(\d{2}):(\d{2})/) {
|
||||
flush_entry();
|
||||
reset_entry();
|
||||
|
||||
my ($yy, $mo, $dd, $hh, $mm, $ss) = ($1, $2, $3, $4, $5, $6);
|
||||
my $yyyy = $yy >= 70 ? 1900 + $yy : 2000 + $yy;
|
||||
|
||||
$ts = timelocal($ss, $mm, $hh, $dd, $mo - 1, $yyyy);
|
||||
$active = 1;
|
||||
next;
|
||||
}
|
||||
|
||||
next unless $active;
|
||||
|
||||
if ($line =~ /^# Query_time:\s*([0-9.]+).*Rows_sent:\s*([0-9]+).*Rows_examined:\s*([0-9]+)/) {
|
||||
$qt = $1 + 0;
|
||||
$rows_sent = $2 + 0;
|
||||
$rows_examined = $3 + 0;
|
||||
next;
|
||||
}
|
||||
|
||||
next if $line =~ /^# User\@Host:/;
|
||||
next if $line =~ /^use `/;
|
||||
next if $line =~ /^SET timestamp=/;
|
||||
next if $line =~ /^#/;
|
||||
next if $line =~ /^\s*$/;
|
||||
|
||||
$sql .= ' ' . $line;
|
||||
}
|
||||
|
||||
flush_entry();
|
||||
|
||||
print "slow_queries_count: $total\n";
|
||||
print "max_query_time: $max_qt\n";
|
||||
print "max_rows_examined: $max_rows_examined\n";
|
||||
print "max_rows_sent: $max_rows_sent\n";
|
||||
print "\n";
|
||||
print "top_fingerprints:\n";
|
||||
|
||||
my $shown = 0;
|
||||
for my $f (
|
||||
sort {
|
||||
$fp{$b}{total_time} <=> $fp{$a}{total_time}
|
||||
||
|
||||
$fp{$b}{total_rows_examined} <=> $fp{$a}{total_rows_examined}
|
||||
} keys %fp
|
||||
) {
|
||||
last if $shown++ >= 10;
|
||||
|
||||
printf(
|
||||
"- count=%d total_time=%.3f max_time=%.3f total_rows_examined=%d sql=%s\n",
|
||||
$fp{$f}{count},
|
||||
$fp{$f}{total_time},
|
||||
$fp{$f}{max_time},
|
||||
$fp{$f}{total_rows_examined},
|
||||
$f
|
||||
);
|
||||
}
|
||||
PERL
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends lsphp process count and RSS stats from an OLS pod to the diag file.
|
||||
# $1 (pod): OLS pod name.
|
||||
function diagOlsPodStats() {
|
||||
local pod="$1"
|
||||
|
||||
diagLogSection "OLS $pod lsphp count and RSS"
|
||||
|
||||
{
|
||||
echo "+ k3sRun exec pod/$pod -c openlitespeed -- sh -s <lsphp stats>"
|
||||
|
||||
k3sRun exec -i "pod/$pod" -c openlitespeed -- sh -s <<'SH' || true
|
||||
tmp="$(mktemp)"
|
||||
|
||||
echo "hostname:"
|
||||
hostname 2>/dev/null || true
|
||||
echo
|
||||
|
||||
ps -eo user=,rss=,comm=,args= > "$tmp" 2>/dev/null || ps aux > "$tmp" 2>/dev/null || true
|
||||
|
||||
echo "lsphp count:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ { c++ }
|
||||
END { print c + 0 }
|
||||
' "$tmp"
|
||||
echo
|
||||
|
||||
echo "total lsphp RSS KiB:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ { sum += $2 }
|
||||
END { print sum + 0 }
|
||||
' "$tmp"
|
||||
echo
|
||||
|
||||
echo "total lsphp RSS MiB:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ { sum += $2 }
|
||||
END { printf "%.1f\n", sum / 1024 }
|
||||
' "$tmp"
|
||||
echo
|
||||
|
||||
echo "top RSS lsphp:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ {
|
||||
print
|
||||
}
|
||||
' "$tmp" | sort -k2,2nr | head -10
|
||||
echo
|
||||
|
||||
echo "lsphp by user:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ {
|
||||
count[$1]++
|
||||
rss[$1] += $2
|
||||
}
|
||||
END {
|
||||
for (u in count) {
|
||||
printf "%s count=%d rss_kib=%d rss_mib=%.1f\n", u, count[u], rss[u], rss[u] / 1024
|
||||
}
|
||||
}
|
||||
' "$tmp" | sort -k3,3nr
|
||||
echo
|
||||
|
||||
echo "process count:"
|
||||
awk '
|
||||
NF >= 3 {
|
||||
comm=$3
|
||||
count[comm]++
|
||||
}
|
||||
END {
|
||||
for (c in count) {
|
||||
print count[c], c
|
||||
}
|
||||
}
|
||||
' "$tmp" | sort -nr | head -20
|
||||
echo
|
||||
|
||||
rm -f "$tmp"
|
||||
SH
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Parses the OLS error log since $diagSince and appends categorized error counters to the diag file.
|
||||
function diagOlsErrorSummary() {
|
||||
local openlitespeedErrorLog="$openlitespeedLogsPath/error.log"
|
||||
local cutoff
|
||||
|
||||
cutoff="$(diagCutoffEpoch)"
|
||||
|
||||
diagLogSection "OLS error counters summary"
|
||||
|
||||
{
|
||||
echo "+ summarize OLS error log: $openlitespeedErrorLog since ${diagSince:-4h}"
|
||||
echo
|
||||
|
||||
if [[ ! -r "$openlitespeedErrorLog" ]]; then
|
||||
echo "ols_error_log_readable: no"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
perl -MTime::Local - "$openlitespeedErrorLog" "$cutoff" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
use Time::Local;
|
||||
|
||||
my ($file, $cutoff) = @ARGV;
|
||||
|
||||
open my $fh, '<', $file or do {
|
||||
print "ols_error_log_readable: no\n";
|
||||
exit 0;
|
||||
};
|
||||
|
||||
my %c = (
|
||||
total_lines => 0,
|
||||
warn_count => 0,
|
||||
error_count => 0,
|
||||
hostname_mapping_conflicts => 0,
|
||||
inaccessible_vhost_root => 0,
|
||||
no_request_delivery => 0,
|
||||
connection_reset => 0,
|
||||
memory_errors => 0,
|
||||
too_many_open_files => 0,
|
||||
permission_denied => 0,
|
||||
);
|
||||
|
||||
while (my $line = <$fh>) {
|
||||
my $ts = 0;
|
||||
|
||||
if ($line =~ /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/) {
|
||||
$ts = timelocal($6, $5, $4, $3, $2 - 1, $1);
|
||||
}
|
||||
|
||||
next if $ts && $ts < $cutoff;
|
||||
|
||||
$c{total_lines}++;
|
||||
$c{warn_count}++ if $line =~ /\[WARN\]/;
|
||||
$c{error_count}++ if $line =~ /\[ERROR\]/;
|
||||
$c{hostname_mapping_conflicts}++ if $line =~ /Hostname .* is mapped to virtual host .* can.t map to virtual host/;
|
||||
$c{inaccessible_vhost_root}++ if $line =~ /Path for vhost root is not accessible/;
|
||||
$c{no_request_delivery}++ if $line =~ /No request delivery notification has been received/;
|
||||
$c{connection_reset}++ if $line =~ /Connection reset by peer/;
|
||||
$c{memory_errors}++ if $line =~ /OOM|out of memory|Cannot allocate memory/i;
|
||||
$c{too_many_open_files}++ if $line =~ /Too many open files/i;
|
||||
$c{permission_denied}++ if $line =~ /Permission denied/i;
|
||||
}
|
||||
|
||||
for my $k (
|
||||
qw(
|
||||
total_lines
|
||||
warn_count
|
||||
error_count
|
||||
hostname_mapping_conflicts
|
||||
inaccessible_vhost_root
|
||||
no_request_delivery
|
||||
connection_reset
|
||||
memory_errors
|
||||
too_many_open_files
|
||||
permission_denied
|
||||
)
|
||||
) {
|
||||
print "$k: $c{$k}\n";
|
||||
}
|
||||
PERL
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Fetches the OPcache status endpoint four times and appends key metrics to the diag file.
|
||||
function diagOpcacheSummary() {
|
||||
local i tmp
|
||||
|
||||
diagLogSection "OPcache summary"
|
||||
|
||||
{
|
||||
echo "+ curl OPcache endpoint summary"
|
||||
echo "attempts: 4"
|
||||
echo
|
||||
|
||||
for i in 1 2 3 4; do
|
||||
tmp="$(mktemp)"
|
||||
|
||||
curl -kfsS --max-time 10 "$diagOpcacheUrl" > "$tmp" 2>/dev/null || {
|
||||
echo "attempt_$i: failed"
|
||||
rm -f "$tmp"
|
||||
continue
|
||||
}
|
||||
|
||||
echo "attempt_$i:"
|
||||
|
||||
awk '
|
||||
function val(line) {
|
||||
sub(/^.*=>[[:space:]]*/, "", line)
|
||||
if (line == "" || line == "=>") return "false"
|
||||
return line
|
||||
}
|
||||
|
||||
/\[memory_usage\]/ { ctx="memory"; next }
|
||||
/\[interned_strings_usage\]/ { ctx="interned"; next }
|
||||
/\[opcache_statistics\]/ { ctx="stats"; next }
|
||||
/\[jit\]/ { ctx="jit"; next }
|
||||
|
||||
/\[opcache_enabled\]/ { print " opcache_enabled:", val($0) }
|
||||
/\[cache_full\]/ { print " cache_full:", val($0) }
|
||||
/\[restart_pending\]/ { print " restart_pending:", val($0) }
|
||||
/\[restart_in_progress\]/ { print " restart_in_progress:", val($0) }
|
||||
|
||||
ctx=="memory" && /\[used_memory\]/ { print " memory_used:", val($0) }
|
||||
ctx=="memory" && /\[free_memory\]/ { print " memory_free:", val($0) }
|
||||
ctx=="memory" && /\[wasted_memory\]/ { print " memory_wasted:", val($0) }
|
||||
ctx=="memory" && /\[current_wasted_percentage\]/ { print " memory_wasted_pct:", val($0) }
|
||||
|
||||
ctx=="interned" && /\[buffer_size\]/ { print " interned_buffer_size:", val($0) }
|
||||
ctx=="interned" && /\[used_memory\]/ { print " interned_used:", val($0) }
|
||||
ctx=="interned" && /\[free_memory\]/ { print " interned_free:", val($0) }
|
||||
ctx=="interned" && /\[number_of_strings\]/ { print " interned_strings:", val($0) }
|
||||
|
||||
ctx=="stats" && /\[num_cached_scripts\]/ { print " num_cached_scripts:", val($0) }
|
||||
ctx=="stats" && /\[num_cached_keys\]/ { print " num_cached_keys:", val($0) }
|
||||
ctx=="stats" && /\[max_cached_keys\]/ { print " max_cached_keys:", val($0) }
|
||||
ctx=="stats" && /\[hits\]/ { print " hits:", val($0) }
|
||||
ctx=="stats" && /\[misses\]/ { print " misses:", val($0) }
|
||||
ctx=="stats" && /\[oom_restarts\]/ { print " oom_restarts:", val($0) }
|
||||
ctx=="stats" && /\[hash_restarts\]/ { print " hash_restarts:", val($0) }
|
||||
ctx=="stats" && /\[manual_restarts\]/ { print " manual_restarts:", val($0) }
|
||||
ctx=="stats" && /\[opcache_hit_rate\]/ { print " opcache_hit_rate:", val($0) }
|
||||
' "$tmp" || true
|
||||
|
||||
rm -f "$tmp"
|
||||
echo
|
||||
done
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Collects system-level diagnostics (uptime, memory, vmstat, iostat, PSI, sar, disk usage).
|
||||
function diagSystem() {
|
||||
diagLogSection "uptime"
|
||||
diagCmd uptime
|
||||
|
||||
diagLogSection "free -m"
|
||||
diagCmd free -m
|
||||
|
||||
diagLogSection "vmstat 1 10"
|
||||
diagCmd vmstat 1 10
|
||||
|
||||
diagLogSection "mpstat 1 10"
|
||||
diagCmd mpstat 1 10
|
||||
|
||||
diagLogSection "iostat -x -z 1 5 filtered"
|
||||
diagIostat
|
||||
|
||||
diagLogSection "cat /proc/pressure/cpu"
|
||||
diagCmd cat /proc/pressure/cpu
|
||||
|
||||
diagLogSection "cat /proc/pressure/memory"
|
||||
diagCmd cat /proc/pressure/memory
|
||||
|
||||
diagLogSection "cat /proc/pressure/io"
|
||||
diagCmd cat /proc/pressure/io
|
||||
|
||||
diagSarTry "sar queue last ${diagSince:-4h}" "sar -q -s '$(diagSarStartTime)'"
|
||||
diagSarTry "sar cpu last ${diagSince:-4h}" "sar -u -s '$(diagSarStartTime)'"
|
||||
diagSarTry "sar memory last ${diagSince:-4h}" "sar -r -s '$(diagSarStartTime)'"
|
||||
diagSarTry "sar swap last ${diagSince:-4h}" "sar -W -s '$(diagSarStartTime)'"
|
||||
|
||||
diagLogSection "Filesystem pressure quick checks"
|
||||
diagDfUsage
|
||||
|
||||
diagLogSection "Filesystem inode quick checks"
|
||||
diagDfInodes
|
||||
}
|
||||
|
||||
# Collects k3s diagnostics (pod top/list, warning events, restart summary, node conditions).
|
||||
function diagK3s() {
|
||||
diagLogSection "Top pod"
|
||||
diagK3sTry top pod
|
||||
|
||||
diagLogSection "Get pod"
|
||||
diagK3sTry get pod
|
||||
|
||||
diagLogSection "Get warning events recent"
|
||||
diagK3sTry get events --field-selector type!=Normal --sort-by=.lastTimestamp
|
||||
|
||||
diagLogSection "Pod restart summary"
|
||||
diagK3sTry get pod -o custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount,STATE:.status.containerStatuses[*].state.waiting.reason,LAST_STATE:.status.containerStatuses[*].lastState.terminated.reason
|
||||
|
||||
diagLogSection "Node conditions and allocated resources"
|
||||
diagK3sNodeSummary
|
||||
}
|
||||
|
||||
# Collects MariaDB diagnostics (master/slave global status, replication summary, slow queries).
|
||||
function diagMariaDB() {
|
||||
diagLogSection "MariaDB master global status"
|
||||
{
|
||||
echo "+ mariadbMasterRootQuery"
|
||||
mariadbMasterRootQuery "
|
||||
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
||||
'Uptime',
|
||||
'Threads_running',
|
||||
'Threads_connected',
|
||||
'Max_used_connections',
|
||||
'Connections',
|
||||
'Aborted_connects',
|
||||
'Slow_queries',
|
||||
'Questions',
|
||||
'Queries',
|
||||
'Created_tmp_tables',
|
||||
'Created_tmp_disk_tables',
|
||||
'Innodb_buffer_pool_reads',
|
||||
'Innodb_buffer_pool_read_requests',
|
||||
'Innodb_data_reads',
|
||||
'Innodb_data_writes',
|
||||
'Innodb_log_waits',
|
||||
'Open_tables',
|
||||
'Opened_tables',
|
||||
'Table_open_cache_hits',
|
||||
'Table_open_cache_misses',
|
||||
'Table_open_cache_overflows'
|
||||
);
|
||||
" || true
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
|
||||
diagLogSection "MariaDB slave global status"
|
||||
{
|
||||
echo "+ mariadbSlaveRootQuery"
|
||||
mariadbSlaveRootQuery "
|
||||
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
||||
'Uptime',
|
||||
'Threads_running',
|
||||
'Threads_connected',
|
||||
'Max_used_connections',
|
||||
'Connections',
|
||||
'Aborted_connects',
|
||||
'Slow_queries',
|
||||
'Questions',
|
||||
'Queries',
|
||||
'Created_tmp_tables',
|
||||
'Created_tmp_disk_tables',
|
||||
'Innodb_buffer_pool_reads',
|
||||
'Innodb_buffer_pool_read_requests',
|
||||
'Innodb_data_reads',
|
||||
'Innodb_data_writes',
|
||||
'Innodb_log_waits',
|
||||
'Slave_running',
|
||||
'Slave_received_heartbeats',
|
||||
'Slave_heartbeat_period',
|
||||
'Slave_open_temp_tables'
|
||||
);
|
||||
" || true
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
|
||||
diagMariaDBReplicaSummary
|
||||
diagMariaDBSlowSummary
|
||||
}
|
||||
|
||||
# Collects OpenLiteSpeed diagnostics (pod list, per-pod lsphp stats, error summary, OPcache).
|
||||
function diagOpenLiteSpeed() {
|
||||
local podList pod
|
||||
|
||||
diagLogSection "OpenLiteSpeed pods"
|
||||
diagK3sTry get pod -l app=openlitespeed
|
||||
|
||||
podList="$(k3sPodListApp openlitespeed 2>/dev/null || true)"
|
||||
while read -r pod; do
|
||||
[[ -z "$pod" ]] && continue
|
||||
diagOlsPodStats "$pod"
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
|
||||
diagOlsErrorSummary
|
||||
diagOpcacheSummary
|
||||
}
|
||||
|
||||
# Generates a full diagnostic report and writes it to $diagFile.
|
||||
# [$1] (diagFile): output file path (defaults to $diagFile).
|
||||
function diagReport() {
|
||||
local diagFile="${1:-$diagFile}"
|
||||
local aiModel="${1:-short}"
|
||||
|
||||
export LC_ALL=C
|
||||
export LANG=C
|
||||
|
||||
local reportPath
|
||||
reportPath=$(dirname "$diagFile")
|
||||
[[ -d "$reportPath" ]] || mkdir -p "$reportPath"
|
||||
|
||||
if [[ "$postfixHost" ]]; then
|
||||
diagExcludeVhostsRegex="$diagExcludeVhostsRegex|${postfixHost//./\\.}"
|
||||
fi
|
||||
|
||||
: > "$diagFile"
|
||||
diagMeta
|
||||
diagSystem
|
||||
diagK3s
|
||||
diagMariaDB
|
||||
diagOpenLiteSpeed
|
||||
diagSanitizeReport
|
||||
}
|
||||
|
||||
# Uploads the diag report file to the remote API.
|
||||
# [$1] (diagFile): report file path (defaults to $diagFile).
|
||||
# [$2] (aiModelId): AI endpoint ID to use (defaults to 1).
|
||||
function diagSend() {
|
||||
local diagFile="${1:-$diagFile}"
|
||||
local aiModelId="${2:-1}"
|
||||
|
||||
logInfo "$diagLabel Sending data..."
|
||||
curl -fsS -X POST "$diagApiUrl" -F "source_type=worker" -F "source_id=$hostUuid" -F "generated_time=$(date +%s)" -F "report_file=@$diagFile" -F "ai_endpoint_id=$aiModelId"
|
||||
logSuccess "$diagLabel Data sent successfully"
|
||||
}
|
||||
|
||||
# Generates the full diagnostic report and sends it to the remote API.
|
||||
# [$1] (aiModel): model name: full or short (defaults to short).
|
||||
function diagRun() {
|
||||
local aiModel="${1:-short}"
|
||||
local aiModelId=2
|
||||
|
||||
case "$aiModel" in
|
||||
full)
|
||||
aiModelId=1
|
||||
;;
|
||||
*)
|
||||
aiModelId=2
|
||||
;;
|
||||
esac
|
||||
|
||||
diagReport "$diagFile"
|
||||
diagSend "$diagFile" "$aiModelId"
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# Waits for the k3s API to become ready by polling /readyz.
|
||||
# Retries up to $k3sReadyRetries times with $k3sReadySleep seconds between attempts.
|
||||
function k3sReady() {
|
||||
local tries=${k3sReadyRetries:-10}
|
||||
local delay=${k3sReadySleep:-2}
|
||||
local i
|
||||
for ((i=1; i<=tries; i++)); do
|
||||
"$k3sCmd" kubectl get --raw=/readyz >/dev/null 2>&1 && return 0
|
||||
sleep "$delay"
|
||||
done
|
||||
|
||||
appError "k3s API not ready after $tries tries"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Validates a YAML file against the Kubernetes API (dry-run).
|
||||
# $1 (file): path to the YAML file.
|
||||
function k3sYamlCheck() {
|
||||
local file="$1"
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
[[ -f "$file" ]] || { appError "File not found: $file"; return 1; }
|
||||
|
||||
k3sReady
|
||||
runFail "$k3sCmd" kubectl apply --dry-run=client -f "$file"
|
||||
}
|
||||
|
||||
# Applies a YAML configuration to Kubernetes.
|
||||
# $1 (file): path to the YAML configuration file.
|
||||
function k3sYamlApply() {
|
||||
local file="$1" output error
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
|
||||
run output error "$k3sCmd" kubectl apply -f "$file" --validate=false --request-timeout=60s || {
|
||||
appError "Error applying YAML: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Runs kubectl in $k3sNamespace after ensuring k3s is ready.
|
||||
function k3sRun() {
|
||||
k3sReady || return 1
|
||||
"$k3sCmd" kubectl -n "$k3sNamespace" "$@"
|
||||
}
|
||||
|
||||
# Lists pod names sorted alphabetically, optionally filtered by app label.
|
||||
# [$1] (app): app label value to filter by (optional).
|
||||
function k3sPodList() {
|
||||
local args=()
|
||||
[[ -n "${1-}" ]] && args+=(-l "app=$1")
|
||||
|
||||
k3sRun get pods "${args[@]}" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' --sort-by='{.metadata.name}'
|
||||
}
|
||||
|
||||
# Lists pod names with their phase (name|phase), optionally filtered by app label.
|
||||
# [$1] (app): app label value to filter by (optional).
|
||||
function k3sPodListStatus() {
|
||||
local args=()
|
||||
[[ -n "${1-}" ]] && args+=(-l "app=$1")
|
||||
|
||||
local raw
|
||||
raw=$(k3sRun get pods "${args[@]}" \
|
||||
-o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.status.phase}{"|"}{range .status.conditions[*]}{.type}{"="}{.status}{" "}{end}{"\n"}{end}' \
|
||||
--sort-by='{.metadata.name}') || return 1
|
||||
|
||||
awk -F'|' 'NF {
|
||||
if ($2 == "Running" && $3 ~ /(^| )DisruptionTarget=True( |$)/)
|
||||
status = "Terminating"
|
||||
else if ($2 == "Running" && $3 !~ /(^| )Ready=True( |$)/)
|
||||
status = "NotReady"
|
||||
else
|
||||
status = $2
|
||||
print $1 "|" status
|
||||
}' <<< "$raw"
|
||||
}
|
||||
|
||||
# Captures the current list of pod names into an array variable.
|
||||
# $1 (varname): name of the array variable to populate.
|
||||
function k3sPodsSnapshot() {
|
||||
local -n __out="$1"
|
||||
mapfile -t __out < <(
|
||||
k3sRun get pods --no-headers 2>/dev/null | awk '{print $1}'
|
||||
)
|
||||
}
|
||||
|
||||
# Waits until all pods not in the baseline snapshot are Running/Succeeded/Completed.
|
||||
# $1 (varname): name of the baseline array variable (from k3sPodsSnapshot).
|
||||
# [$2] (timeout): max wait in seconds (default 240).
|
||||
# [$3] (interval): poll interval in seconds (default 4).
|
||||
function k3sWaitNewPodsReady() {
|
||||
local -n baseline="$1"
|
||||
local timeout="${2:-240}"
|
||||
local interval="${3:-4}"
|
||||
|
||||
local attempts=$(( timeout / interval ))
|
||||
(( attempts < 1 )) && attempts=1
|
||||
|
||||
local i notReady newSeen
|
||||
for ((i=1; i<=attempts; i++)); do
|
||||
mapfile -t _now < <(
|
||||
k3sRun get pods --no-headers 2>/dev/null | awk '{print $1, $3}'
|
||||
)
|
||||
|
||||
notReady=0
|
||||
newSeen=0
|
||||
local ln name status
|
||||
for ln in "${_now[@]}"; do
|
||||
[[ -z "$ln" ]] && continue
|
||||
name="${ln%% *}"
|
||||
status="${ln#* }"
|
||||
|
||||
if ! arrayContains "$name" "${baseline[@]}"; then
|
||||
newSeen=1
|
||||
if [[ "$status" != "Running" && "$status" != "Succeeded" && "$status" != "Completed" ]]; then
|
||||
((notReady++))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $newSeen -eq 0 || $notReady -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $i -lt $attempts ]]; then
|
||||
printDotText "pods not ready: $fontBlue$notReady$fontReset" "${fontYellow}waiting$fontReset"
|
||||
sleep "$interval"
|
||||
else
|
||||
printDotText "pods not ready: $fontBlue$notReady$fontReset" "${fontRed}waiting time's up$fontReset"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
# Executes a command inside the MariaDB master pod.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function mariadbMasterExec() {
|
||||
k3sRun exec pod/"$mariadbMasterKube"-0 -c "$mariadbMasterKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside the MariaDB master pod with stdin attached (-i).
|
||||
# $@ (...): command and arguments to execute.
|
||||
function mariadbMasterExecI() {
|
||||
k3sRun exec -i pod/"$mariadbMasterKube"-0 -c "$mariadbMasterKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside the MariaDB slave pod.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function mariadbSlaveExec() {
|
||||
k3sRun exec pod/"$mariadbSlaveKube"-0 -c "$mariadbSlaveKube" -- "$@"
|
||||
}
|
||||
|
||||
# Runs a SQL query as root against the specified MariaDB pod.
|
||||
# $1 (target): master|slave
|
||||
# $2 (query): SQL query.
|
||||
# [$3] (showColumn): pass "showColumn" to keep column headers.
|
||||
function mariadbRootQuery() {
|
||||
local target="$1" query="$2"
|
||||
[[ -n "$query" ]] || { appError "Query not specified"; return 1; }
|
||||
|
||||
local execFn
|
||||
case "$target" in
|
||||
master) execFn=mariadbMasterExec ;;
|
||||
slave) execFn=mariadbSlaveExec ;;
|
||||
*) appError "Unknown target: $target"; return 1 ;;
|
||||
esac
|
||||
|
||||
if [[ "${3-}" == "showColumn" ]]; then
|
||||
"$execFn" mariadb -uroot -p"$mariadbRootPass" -e "$query"
|
||||
else
|
||||
"$execFn" mariadb -uroot -p"$mariadbRootPass" -N -e "$query"
|
||||
fi
|
||||
}
|
||||
|
||||
# Runs a SQL query as root against the MariaDB master pod.
|
||||
# $1 (query): SQL query.
|
||||
# [$2] (showColumn): pass "showColumn" to keep column headers.
|
||||
function mariadbMasterRootQuery() { mariadbRootQuery master "$@"; }
|
||||
# Runs a SQL query as root against the MariaDB slave pod.
|
||||
# $1 (query): SQL query.
|
||||
# [$2] (showColumn): pass "showColumn" to keep column headers.
|
||||
function mariadbSlaveRootQuery() { mariadbRootQuery slave "$@"; }
|
||||
|
||||
# Converts a domain name into a MariaDB-safe identifier (max 60 chars).
|
||||
# $1 (domain): domain name.
|
||||
function mariadbDomain2id() {
|
||||
domainToRandom "$1" 60
|
||||
}
|
||||
|
||||
# Reads the MariaDB root password from the Kubernetes secret.
|
||||
function mariadbRootPassSecretGet() {
|
||||
k3sRun get secret "$mariadbKube-secret" -o jsonpath="{.data.root-password}" --ignore-not-found | base64 -d
|
||||
}
|
||||
|
||||
# Saves the MariaDB root password from the Kubernetes secret to a local file.
|
||||
function mariadbRootPassSecretSave() {
|
||||
local password
|
||||
password="$(mariadbRootPassSecretGet)"
|
||||
[[ -n "$password" ]] && printf '%s\n' "$password" > "$appDataPath/$hostName.$mariadbKube"
|
||||
}
|
||||
|
||||
# Lists user-created MariaDB databases (system schemas excluded).
|
||||
function mariadbDatabaseListGet() {
|
||||
local output error
|
||||
run output error mariadbMasterRootQuery "SHOW DATABASES;" || { appError "$error"; return 1; }
|
||||
printf '%s\n' "$output" | awk '!/^(information_schema|performance_schema|mysql|sys)$/'
|
||||
}
|
||||
|
||||
# Lists MariaDB users with Host=% (root and replication_user excluded).
|
||||
function mariadbUserListGet() {
|
||||
local output error
|
||||
run output error mariadbMasterRootQuery "SELECT user FROM mysql.user WHERE Host = '%';" || { appError "$error"; return 1; }
|
||||
printf '%s\n' "$output" | awk '!/^(root|replication_user)$/'
|
||||
}
|
||||
|
||||
# Creates or updates a MariaDB database and user with full privileges.
|
||||
# Revokes privileges from any other users previously assigned to the same database.
|
||||
# $1 (database): database name.
|
||||
# $2 (username): database username.
|
||||
# $3 (password): database user password.
|
||||
function mariadbDatabaseUserSet() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
local username="$2"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$3"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
mariadbMasterRootQuery "CREATE DATABASE IF NOT EXISTS \`$database\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS '$username'@'%' IDENTIFIED BY '$password'; ALTER USER '$username'@'%' IDENTIFIED BY '$password'; GRANT ALL PRIVILEGES ON \`$database\`.* TO '$username'@'%' WITH MAX_USER_CONNECTIONS 15;" || {
|
||||
appError "Create/update database user failed: database=$database username=$username"
|
||||
return 1
|
||||
}
|
||||
|
||||
local others
|
||||
others="$(mariadbMasterRootQuery "SELECT DISTINCT User, Host FROM mysql.db WHERE Db='$database' AND NOT (User='$username' AND Host='%');")"
|
||||
while IFS=$'\t' read -r u h; do
|
||||
[[ -z "$u" || -z "$h" ]] && continue
|
||||
mariadbMasterRootQuery "REVOKE ALL PRIVILEGES ON \`$database\`.* FROM '$u'@'$h';" || \
|
||||
appError "Revoke privileges failed: database=$database user=$u host=$h"
|
||||
done <<< "$others"
|
||||
}
|
||||
|
||||
# Checks whether a MariaDB database exists.
|
||||
# $1 (database): database name.
|
||||
function mariadbDatabaseExists() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
|
||||
local out
|
||||
out="$(mariadbMasterRootQuery "SHOW DATABASES LIKE '$database';" 2>/dev/null | tail -n 1 | tr -d '\r')"
|
||||
[[ "$out" == "$database" ]]
|
||||
}
|
||||
|
||||
# Removes a MariaDB database if it exists.
|
||||
# $1 (database): database name.
|
||||
function mariadbDatabaseRemove() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
mariadbMasterRootQuery "DROP DATABASE IF EXISTS \`$database\`;"
|
||||
}
|
||||
|
||||
# Removes a MariaDB user from all hosts.
|
||||
# $1 (username): MariaDB username.
|
||||
function mariadbUserRemove() {
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
|
||||
local hosts
|
||||
hosts="$(mariadbMasterRootQuery "SELECT Host FROM mysql.user WHERE User='$username';")"
|
||||
while read -r host; do
|
||||
mariadbMasterRootQuery "DROP USER IF EXISTS '$username'@'$host';"
|
||||
done <<< "$hosts"
|
||||
}
|
||||
|
||||
# Removes all tables from a MariaDB database.
|
||||
# $1 (database): database name.
|
||||
# $2 (username): database username.
|
||||
# $3 (password): database user password.
|
||||
function mariadbDatabaseClean() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
local username="$2"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$3"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
local output error
|
||||
run output error mariadbMasterExec mariadb -u "$username" -p"$password" -N -e "SELECT CONCAT('DROP TABLE \`', table_name, '\`;') FROM information_schema.tables WHERE table_schema = '$database';" || { appError "$error"; return 1; }
|
||||
run output error mariadbMasterExec mariadb -u "$username" -p"$password" -e "USE \`$database\`; SET FOREIGN_KEY_CHECKS = 0; $output SET FOREIGN_KEY_CHECKS = 1;" || { appError "$error"; return 1; }
|
||||
}
|
||||
|
||||
# Base: runs mariadb-dump via the given exec function.
|
||||
# $1 (execFn): mariadbMasterExec|mariadbSlaveExec.
|
||||
# $2 (username), $3 (password), $4 (database|all).
|
||||
# [$5] (file): auto-generated if omitted.
|
||||
# [$6+] (...): optional mariadb-dump parameters.
|
||||
function mariadbExport() {
|
||||
local execFn="$1"
|
||||
[[ -n "$execFn" ]] || { appError "Exec function not specified"; return 1; }
|
||||
shift
|
||||
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
shift
|
||||
|
||||
local password="$1"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
shift
|
||||
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
shift
|
||||
|
||||
local file="$1"
|
||||
if [[ -z "$file" ]]; then
|
||||
[[ "$database" == "all" ]] \
|
||||
&& file="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_full.sql.tar.gz" \
|
||||
|| file="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_$database.sql.tar.gz"
|
||||
fi
|
||||
shift
|
||||
|
||||
local -a params
|
||||
if [[ $# -gt 0 ]]; then
|
||||
params=("$@")
|
||||
local haveAll=false haveDatabases=false havePositional=false
|
||||
for a in "${params[@]}"; do
|
||||
case "$a" in
|
||||
--all-databases|-A) haveAll=true ;;
|
||||
--databases|-B) haveDatabases=true ;;
|
||||
-*) ;;
|
||||
*) havePositional=true ;;
|
||||
esac
|
||||
done
|
||||
if ! $haveAll && ! $haveDatabases && ! $havePositional; then
|
||||
[[ "$database" == "all" ]] && params+=(--all-databases) || params+=("$database")
|
||||
fi
|
||||
else
|
||||
if [[ "$database" == "all" ]]; then
|
||||
params=(--all-databases --single-transaction --quick --master-data=2 --routines --events)
|
||||
else
|
||||
params=("$database" --single-transaction --quick --routines --events)
|
||||
fi
|
||||
fi
|
||||
|
||||
local filePath fileBase fileSql
|
||||
filePath=$(dirname -- "$file")
|
||||
fileBase=$(basename -- "$file")
|
||||
case "$fileBase" in
|
||||
*.zip) fileSql="${fileBase%.zip}" ;;
|
||||
*.tar.gz) fileSql="${fileBase%.tar.gz}" ;;
|
||||
*) fileSql="$fileBase" ;;
|
||||
esac
|
||||
|
||||
mkdir -p -- "$filePath" || { appError "Failed to create directory: $filePath"; return 1; }
|
||||
rm -f -- "$filePath/$fileSql" "$filePath/$fileBase" &>/dev/null
|
||||
|
||||
local output error
|
||||
runShell output error "$execFn" mariadb-dump -u "$username" -p"$password" "${params[@]}" ">" "$filePath/$fileSql" || {
|
||||
rm -f -- "$filePath/$fileSql" &>/dev/null
|
||||
appError "Error creating dump: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$fileBase" in
|
||||
*.zip|*.tar.gz|*.tgz)
|
||||
runError error archiveCreate "$filePath/$fileSql" "$file" || {
|
||||
rm -f -- "$filePath/$fileSql" "$filePath/$fileBase" &>/dev/null
|
||||
appError "Error creating archive dump: $error"
|
||||
return 1
|
||||
}
|
||||
rm -f -- "$filePath/$fileSql" &>/dev/null
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s' "$file"
|
||||
}
|
||||
|
||||
# Exports from the master pod.
|
||||
# $1 (username), $2 (password), $3 (database|all), [$4] (file), [$5+] (params)
|
||||
function mariadbMasterExport() {
|
||||
mariadbExport mariadbMasterExec "$@"
|
||||
}
|
||||
|
||||
# Exports using root credentials.
|
||||
# $1 (execFn), $2 (database|all), [$3] (file), [$4+] (params)
|
||||
function mariadbRootExport() {
|
||||
mariadbExport "$1" root "$mariadbRootPass" "${@:2}"
|
||||
}
|
||||
|
||||
# Exports from master using root credentials.
|
||||
# [$1] (database|all), [$2] (file), [$3+] (params)
|
||||
function mariadbMasterRootExport() {
|
||||
local target="${1:-all}"
|
||||
local file="${2:-$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_full.sql.tar.gz}"
|
||||
mariadbRootExport mariadbMasterExec "$target" "$file" ${@:3}
|
||||
}
|
||||
|
||||
# Exports from slave using root credentials.
|
||||
# [$1] (database|all), [$2] (file)
|
||||
function mariadbSlaveRootExport() {
|
||||
local target="${1:-all}"
|
||||
local file="${2:-$appDataPath/$mariadbSlaveKube/${appDate}_${appTime}_full.sql.tar.gz}"
|
||||
mariadbRootExport mariadbSlaveExec "$target" "$file" --all-databases --single-transaction --quick --routines --events
|
||||
}
|
||||
|
||||
# Imports a MariaDB dump into a database.
|
||||
# Supports plain SQL, .zip and .tar.gz archives (must contain exactly one file).
|
||||
# $1 (database|all): target database, or "all" to import without selecting a database.
|
||||
# $2 (username): username used for import.
|
||||
# $3 (password): password used for import.
|
||||
# $4 (file): source dump file.
|
||||
function mariadbMasterImport() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
local username="$2"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$3"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
local file="$4"
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
[[ -f "$file" ]] || { appError "File not found: $file"; return 1; }
|
||||
|
||||
local tmpFile
|
||||
if [[ "$file" == *.zip ]]; then
|
||||
local entriesRaw
|
||||
entriesRaw="$(unzip -Z1 "$file" 2>/dev/null)" || { appError "Not a valid zip archive: $file"; return 1; }
|
||||
|
||||
local -a entries=()
|
||||
mapfile -t entries < <(printf '%s\n' "$entriesRaw" | sed '/\/$/d')
|
||||
[[ ${#entries[@]} -eq 1 ]] || { appError "Archive must contain exactly one file: $file"; return 1; }
|
||||
|
||||
tmpFile="$(mktemp)" || { appError "Failed to create temporary file"; return 1; }
|
||||
unzip -p "$file" "${entries[0]}" > "$tmpFile" 2>/dev/null || {
|
||||
rm -f -- "$tmpFile"
|
||||
appError "Failed to extract ZIP archive"
|
||||
return 1
|
||||
}
|
||||
elif [[ "$file" == *.tar.gz ]]; then
|
||||
local entriesRaw
|
||||
entriesRaw="$(tar -tzf "$file" 2>/dev/null)" || { appError "Not a valid tar.gz archive: $file"; return 1; }
|
||||
|
||||
local -a entries=()
|
||||
mapfile -t entries < <(printf '%s\n' "$entriesRaw" | sed '/\/$/d')
|
||||
[[ ${#entries[@]} -eq 1 ]] || { appError "Archive must contain exactly one file: $file"; return 1; }
|
||||
|
||||
tmpFile="$(mktemp)" || { appError "Failed to create temporary file"; return 1; }
|
||||
tar -xOzf "$file" "${entries[0]}" > "$tmpFile" 2>/dev/null || {
|
||||
rm -f -- "$tmpFile"
|
||||
appError "Failed to extract tar.gz archive"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
tmpFile="$file"
|
||||
fi
|
||||
|
||||
if [[ ! -s "$tmpFile" ]]; then
|
||||
[[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile"
|
||||
appError "The SQL dump is empty"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a mariadbCmd=(mariadbMasterExecI mariadb -u "$username" -p"$password")
|
||||
[[ "$database" != "all" ]] && mariadbCmd+=("$database")
|
||||
|
||||
local output error
|
||||
runShell output error "${mariadbCmd[@]}" "<" "$tmpFile" ">" /dev/null || {
|
||||
[[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile"
|
||||
appError "Import failed: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
|
||||
[[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile"
|
||||
}
|
||||
|
||||
# Imports a MariaDB dump using root credentials.
|
||||
# $1 (database|all): target database, or "all".
|
||||
# $2 (file): source dump file.
|
||||
function mariadbMasterRootImport() {
|
||||
mariadbMasterImport "$1" root "$mariadbRootPass" "${@:2}"
|
||||
}
|
||||
|
||||
# Creates or updates the MariaDB database and user for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function mariadbConfigRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local databaseName databaseUser databasePass
|
||||
databaseName=$(siteConfigGetOrSet "$domain" "databaseName" "$(mariadbDomain2id "$domain")")
|
||||
databaseUser=$(siteConfigGetOrSet "$domain" "databaseUser" "$(mariadbDomain2id "$domain")")
|
||||
databasePass=$(siteConfigGetOrCreate "$domain" "databasePass")
|
||||
mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass" || return 1
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
# Restarts the vmagent deployment; errors are intentionally ignored.
|
||||
function metricRestart() {
|
||||
k3sRun rollout restart deployment/"$metricKube"-vmagent || true
|
||||
}
|
||||
|
||||
# Writes Prometheus metrics (build info, vhost counts, domain counts, pod memory) to $metricPromPath/kube.prom.
|
||||
function metricKube() {
|
||||
local fileProm="$metricPromPath/kube.prom"
|
||||
local fileTmp="${fileProm}.tmp"
|
||||
|
||||
mkdir -p -- "$metricPromPath" || return 1
|
||||
|
||||
local vhostAllCount vhostUpCount
|
||||
vhostAllCount=$(openlitespeedVhostList all | grep -c . || true)
|
||||
vhostUpCount=$(openlitespeedVhostList up | grep -c . || true)
|
||||
|
||||
local appTimestamp
|
||||
appTimestamp=$(date -d "$appDate ${appTime//-/:}" +%s)
|
||||
|
||||
local domainAllCount=-1 domainFreeCount=-1
|
||||
[[ -f "$domainsList" ]] && domainAllCount=$(grep -cP '^(?!\s*$)' "$domainsList" || true)
|
||||
[[ -f "$domainsList" ]] && domainFreeCount=$(grep -c '^#' "$domainsList" || true)
|
||||
|
||||
local masterMemReq=-1 masterMemLim=-1
|
||||
local slaveMemReq=-1 slaveMemLim=-1
|
||||
local olsMemReq=-1 olsMemLim=-1
|
||||
local redisMemReq=-1 redisMemLim=-1
|
||||
local _line _req _lim
|
||||
|
||||
_line=$(k3sRun get pod "${mariadbMasterKube}-0" \
|
||||
-o jsonpath="{.spec.containers[?(@.name=='${mariadbMasterKube}')].resources.requests.memory} {.spec.containers[?(@.name=='${mariadbMasterKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
masterMemReq=$(sizeToBytes "$_req"); masterMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
_line=$(k3sRun get pod "${mariadbSlaveKube}-0" \
|
||||
-o jsonpath="{.spec.containers[?(@.name=='${mariadbSlaveKube}')].resources.requests.memory} {.spec.containers[?(@.name=='${mariadbSlaveKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
slaveMemReq=$(sizeToBytes "$_req"); slaveMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
_line=$(k3sRun get pods -l "app=$openlitespeedKube" \
|
||||
-o jsonpath="{.items[0].spec.containers[?(@.name=='${openlitespeedKube}')].resources.requests.memory} {.items[0].spec.containers[?(@.name=='${openlitespeedKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
olsMemReq=$(sizeToBytes "$_req"); olsMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
_line=$(k3sRun get pod "${redisKube}-0" \
|
||||
-o jsonpath="{.spec.containers[?(@.name=='${redisKube}')].resources.requests.memory} {.spec.containers[?(@.name=='${redisKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
redisMemReq=$(sizeToBytes "$_req"); redisMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
cat > "$fileTmp" <<EOF
|
||||
# HELP kube_build_info Build and version info
|
||||
# TYPE kube_build_info gauge
|
||||
kube_build_info{version="${appVersion}"} 1
|
||||
|
||||
# HELP kube_start_time Time snapshot
|
||||
# TYPE kube_start_time gauge
|
||||
kube_start_time $appTimestamp
|
||||
|
||||
# HELP kube_vhost_all_count Number of virtual hosts
|
||||
# TYPE kube_vhost_all_count gauge
|
||||
kube_vhost_all_count $vhostAllCount
|
||||
|
||||
# HELP kube_vhost_up_count Number of active virtual hosts
|
||||
# TYPE kube_vhost_up_count gauge
|
||||
kube_vhost_up_count $vhostUpCount
|
||||
|
||||
# HELP kube_domain_all_count Number of domains
|
||||
# TYPE kube_domain_all_count gauge
|
||||
kube_domain_all_count $domainAllCount
|
||||
|
||||
# HELP kube_domain_use_count Number of use domains
|
||||
# TYPE kube_domain_use_count gauge
|
||||
kube_domain_use_count $domainFreeCount
|
||||
|
||||
# HELP kube_pod_memory_request_bytes Pod memory request in bytes
|
||||
# TYPE kube_pod_memory_request_bytes gauge
|
||||
kube_pod_memory_request_bytes{component="mariadb-master"} $masterMemReq
|
||||
kube_pod_memory_request_bytes{component="mariadb-slave"} $slaveMemReq
|
||||
kube_pod_memory_request_bytes{component="openlitespeed"} $olsMemReq
|
||||
kube_pod_memory_request_bytes{component="redis"} $redisMemReq
|
||||
|
||||
# HELP kube_pod_memory_limit_bytes Pod memory limit in bytes
|
||||
# TYPE kube_pod_memory_limit_bytes gauge
|
||||
kube_pod_memory_limit_bytes{component="mariadb-master"} $masterMemLim
|
||||
kube_pod_memory_limit_bytes{component="mariadb-slave"} $slaveMemLim
|
||||
kube_pod_memory_limit_bytes{component="openlitespeed"} $olsMemLim
|
||||
kube_pod_memory_limit_bytes{component="redis"} $redisMemLim
|
||||
EOF
|
||||
mv "$fileTmp" "$fileProm"
|
||||
}
|
||||
|
||||
# Collects lsphp CPU/memory/worker metrics per domain across OLS pods and writes to $metricPromPath/lsphp.prom.
|
||||
function metricLsphp() {
|
||||
local fileProm="$metricPromPath/lsphp.prom"
|
||||
local fileTmp="${fileProm}.tmp"
|
||||
|
||||
mkdir -p -- "$metricPromPath" || return 1
|
||||
|
||||
local podList error
|
||||
run podList error k3sPodListStatus "$openlitespeedKube" || return 1
|
||||
|
||||
local pod phase output cpu mem count domain
|
||||
{
|
||||
printf '# HELP lsphp_cpu_percent Total CPU percent used by lsphp workers per domain\n'
|
||||
printf '# TYPE lsphp_cpu_percent gauge\n'
|
||||
printf '# HELP lsphp_memory_percent Total memory percent used by lsphp workers per domain\n'
|
||||
printf '# TYPE lsphp_memory_percent gauge\n'
|
||||
printf '# HELP lsphp_worker_count Number of lsphp worker processes per domain\n'
|
||||
printf '# TYPE lsphp_worker_count gauge\n'
|
||||
|
||||
while IFS='|' read -r pod phase; do
|
||||
[[ "$phase" == "Running" ]] || continue
|
||||
run output error openlitespeedPodExec "$pod" sh -c "ps aux | grep lsphp | grep -v grep | awk '{u=\$1;cpu[u]+=\$3;mem[u]+=\$4;count[u]++} END {for(u in cpu){name=u;cmd=\"find /var/www/data -maxdepth 1 -uid \"u\" -type d 2>/dev/null\";if((cmd|getline dir)>0&&dir!=\"\"){n=split(dir,a,\"/\");name=a[n]};close(cmd);print cpu[u],mem[u],count[u],name}}'" || continue
|
||||
while IFS=' ' read -r cpu mem count domain; do
|
||||
[[ -n "$domain" ]] || continue
|
||||
printf 'lsphp_cpu_percent{domain="%s",pod="%s"} %s\n' "$domain" "$pod" "$cpu"
|
||||
printf 'lsphp_memory_percent{domain="%s",pod="%s"} %s\n' "$domain" "$pod" "$mem"
|
||||
printf 'lsphp_worker_count{domain="%s",pod="%s"} %s\n' "$domain" "$pod" "$count"
|
||||
done <<< "$output"
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
} > "$fileTmp"
|
||||
|
||||
mv "$fileTmp" "$fileProm"
|
||||
}
|
||||
|
||||
# Collects disk usage per site and sends metrics to the API.
|
||||
function metricSites() {
|
||||
local metricSitesApiUrl="https://api.sodew.ai/api/metrics/sites"
|
||||
local batchId batchTime metricsJson dataJson payload httpCode
|
||||
batchId="$(uuidgen | tr '[:upper:]' '[:lower:]')"
|
||||
batchTime="$(date +%s)"
|
||||
metricsJson='{
|
||||
"site_disk_usage_mb": {"type":"number"}
|
||||
}'
|
||||
dataJson='{}'
|
||||
|
||||
local error vhostList
|
||||
run vhostList error openlitespeedVhostList all || {
|
||||
appError "Error retrieving vhost list: $error"
|
||||
return 1
|
||||
}
|
||||
while IFS= read -r domain <&3; do
|
||||
local diskUsage domainJson
|
||||
diskUsage="$(pathSize "$vhostsPath/$domain" "mb" || true)"
|
||||
[[ -n "$diskUsage" ]] || continue
|
||||
|
||||
domainJson="$(
|
||||
jq -n --arg diskUsage "$diskUsage" \
|
||||
'{
|
||||
site_disk_usage_mb: { value: (if ($diskUsage | length) > 0 then ($diskUsage | tonumber) else null end) },
|
||||
}'
|
||||
)"
|
||||
dataJson="$(jq --arg domain "$domain" --argjson row "$domainJson" '. + {($domain): $row}' <<< "$dataJson")"
|
||||
done 3< <(awk 'NF' <<< "$vhostList")
|
||||
|
||||
payload="$(
|
||||
jq -n \
|
||||
--arg source_type "worker" \
|
||||
--arg source_id "$hostUuid" \
|
||||
--arg batch_id "$batchId" \
|
||||
--argjson batch_time "$batchTime" \
|
||||
--argjson metrics "$metricsJson" \
|
||||
--argjson data "$dataJson" \
|
||||
'{
|
||||
source_type: $source_type,
|
||||
source_id: $source_id,
|
||||
batch_id: $batch_id,
|
||||
batch_time: $batch_time,
|
||||
metrics: $metrics,
|
||||
data: $data
|
||||
}'
|
||||
)"
|
||||
|
||||
# Sending data...
|
||||
httpCode="$(curl -sS -o /tmp/metrics_sites_response.txt -w '%{http_code}' -X POST "$metricSitesApiUrl" -H 'Content-Type: application/json' --data-binary "$payload")"
|
||||
if [[ "$httpCode" != "204" ]]; then
|
||||
appError "Ingest failed, HTTP $httpCode"
|
||||
cat /tmp/metrics_sites_response.txt >&2 || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
# [ Config editor ] ===========================================================
|
||||
|
||||
# Normalizes an OpenLiteSpeed configuration file.
|
||||
# Collapses three or more consecutive blank lines into two.
|
||||
# [$1] (file): config file path (defaults to $openlitespeedConfigFile).
|
||||
function openlitespeedConfigNormalize() {
|
||||
local file="${1:-$openlitespeedConfigFile}"
|
||||
[[ -n "$file" ]] || { appError "Config file not specified"; return 1; }
|
||||
|
||||
perl -0pi -e 's/\n{3,}/\n\n/g' "$file" || {
|
||||
appError "Failed normalize config file: $file"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Edits an OpenLiteSpeed configuration file via the OLS config editor script.
|
||||
# $1 (file): config file path.
|
||||
# $2 (mode): edit mode passed to ols-editor.pl.
|
||||
# $3+ (...): additional editor arguments.
|
||||
function openlitespeedConfigEditFile() {
|
||||
local file="$1"
|
||||
local mode="$2"
|
||||
shift 2 || return 1
|
||||
|
||||
[[ -n "$file" ]] || { appError "Config file not specified"; return 1; }
|
||||
[[ -n "$mode" ]] || { appError "Edit mode not specified"; return 1; }
|
||||
|
||||
perl "$appPath/libs/modules/assets/ols-editor.pl" "$file" "$mode" "$@" || {
|
||||
appError "Failed edit config file: $file | mode=$mode"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Edits the main OLS config file.
|
||||
# $@ (...): edit mode and arguments.
|
||||
function openlitespeedConfigEdit() {
|
||||
openlitespeedConfigEditFile "$openlitespeedConfigFile" "$@"
|
||||
}
|
||||
|
||||
# Edits the OLS WebAdmin config file.
|
||||
# $@ (...): edit mode and arguments.
|
||||
function openlitespeedAdminConfigEdit() {
|
||||
openlitespeedConfigEditFile "$openlitespeedAdminPath/admin_config.conf" "$@"
|
||||
}
|
||||
|
||||
# Deletes a value from the main OLS config.
|
||||
function openlitespeedConfigEditDel() {
|
||||
openlitespeedConfigEdit del "$@"
|
||||
}
|
||||
|
||||
# Deletes masked values from the main OLS config.
|
||||
function openlitespeedConfigEditDelMasked() {
|
||||
openlitespeedConfigEdit del_masked "$@"
|
||||
}
|
||||
|
||||
# Adds a value to the main OLS config.
|
||||
function openlitespeedConfigEditAdd() {
|
||||
openlitespeedConfigEdit add "$@"
|
||||
}
|
||||
|
||||
# Sets a value in the main OLS config.
|
||||
function openlitespeedConfigEditSet() {
|
||||
openlitespeedConfigEdit set "$@"
|
||||
}
|
||||
|
||||
# Sets a masked value in the main OLS config.
|
||||
function openlitespeedConfigEditSetMasked() {
|
||||
openlitespeedConfigEdit set_masked "$@"
|
||||
}
|
||||
|
||||
# Adds a virtual host entry to the main OLS config.
|
||||
# $1 (domain): virtual host name.
|
||||
function openlitespeedConfigVHostAdd() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
openlitespeedConfigEdit vhost_add "$domain"
|
||||
}
|
||||
|
||||
# Deletes a virtual host entry from the main OLS config.
|
||||
# $1 (domain): virtual host name.
|
||||
function openlitespeedConfigVHostDel() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
openlitespeedConfigEdit vhost_del "$domain"
|
||||
}
|
||||
|
||||
# Sets a value in the OLS WebAdmin config.
|
||||
function openlitespeedAdminConfigEditSet() {
|
||||
openlitespeedAdminConfigEdit set "$@"
|
||||
}
|
||||
|
||||
# Lists OLS virtual hosts filtered by state.
|
||||
# [$1] (type): filter type: all, up, or down (defaults to all).
|
||||
function openlitespeedVhostList() {
|
||||
local type="${1:-all}"
|
||||
arrayContains "$type" "all" "up" "down" || { appError "Unknown type: $type"; return 1; }
|
||||
openlitespeedConfigEdit vhost_list "$type"
|
||||
}
|
||||
|
||||
# Lists OLS virtual hosts with their map entries, filtered by state.
|
||||
# [$1] (type): filter type: all, up, or down (defaults to all).
|
||||
function openlitespeedVhostListMap() {
|
||||
local type="${1:-all}"
|
||||
arrayContains "$type" "all" "up" "down" || { appError "Unknown type: $type"; return 1; }
|
||||
openlitespeedConfigEdit vhost_list_map "$type"
|
||||
}
|
||||
|
||||
# Lists configured OLS aliases.
|
||||
function openlitespeedAliasList() {
|
||||
openlitespeedConfigEdit alias_list
|
||||
}
|
||||
|
||||
# Lists aliases assigned to a virtual host.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVhostAlias() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
openlitespeedConfigEdit vhost_alias "$domain"
|
||||
}
|
||||
|
||||
# Executes a command inside the OLS deployment container.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function openlitespeedExec() {
|
||||
k3sRun exec deploy/"$openlitespeedKube" -c "$openlitespeedKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside a specific OLS pod.
|
||||
# $1 (pod): pod name.
|
||||
# $2+ (...): command and arguments to execute.
|
||||
function openlitespeedPodExec() {
|
||||
local pod="$1"
|
||||
[[ -n "$pod" ]] || { appError "Pod not specified"; return 1; }
|
||||
shift
|
||||
|
||||
k3sRun exec pod/"$pod" -c "$openlitespeedKube" -- "$@"
|
||||
}
|
||||
|
||||
# Rebuilds filesystem layout, ownership, and permissions for a virtual host.
|
||||
# $1 (vhostPath): virtual host chroot path.
|
||||
# $2 (vhostDataPath): virtual host data path.
|
||||
# $3 (ug): system user/group name for the site.
|
||||
function openlitespeedVhostRebuildPath() {
|
||||
local vhostPath="$1"
|
||||
local vhostDataPath="$2"
|
||||
local ug="$3"
|
||||
[[ -n "$vhostPath" ]] || { appError "vhostPath not specified"; return 1; }
|
||||
[[ -n "$vhostDataPath" ]] || { appError "vhostDataPath not specified"; return 1; }
|
||||
[[ -n "$ug" ]] || { appError "ug not specified"; return 1; }
|
||||
|
||||
# Create site directories
|
||||
mkdir -p -- "$vhostPath"/{www,tmp,session} || { appError "Create vhost directories failed: $vhostPath"; return 1; }
|
||||
|
||||
# Chroot directory: owned by root (required for OpenSSH)
|
||||
chown root:root -- "$vhostPath" || { appError "Change owner of chroot directory failed: $vhostPath"; return 1; }
|
||||
chmod 755 -- "$vhostPath" || { appError "Change permission of chroot directory failed: $vhostPath"; return 1; }
|
||||
|
||||
# Site directories: owned by site user
|
||||
chown -R -- "$ug:$ug" "$vhostPath/www" "$vhostPath/tmp" "$vhostPath/session" || { appError "Change owner of site directories failed: $vhostPath"; return 1; }
|
||||
|
||||
# Permissions: www
|
||||
find "$vhostPath/www" -type d -exec chmod 2750 -- {} + || { appError "Change permissions of www directories failed"; return 1; }
|
||||
find "$vhostPath/www" -type f -exec chmod 640 -- {} + || { appError "Change permissions of www files failed"; return 1; }
|
||||
|
||||
# Permissions: tmp
|
||||
find "$vhostPath/tmp" -type d -exec chmod 700 -- {} + || { appError "Change permissions of tmp directories failed"; return 1; }
|
||||
find "$vhostPath/tmp" -type f -exec chmod 600 -- {} + || { appError "Change permissions of tmp files failed"; return 1; }
|
||||
|
||||
# Permissions: session
|
||||
find "$vhostPath/session" -type d -exec chmod 700 -- {} + || { appError "Change permissions of session directories failed"; return 1; }
|
||||
find "$vhostPath/session" -type f -exec chmod 600 -- {} + || { appError "Change permissions of session files failed"; return 1; }
|
||||
|
||||
# Vhost data directory and bootstrap.php
|
||||
mkdir -p -- "$vhostDataPath" || { appError "Create vhost data directory failed: $vhostDataPath"; return 1; }
|
||||
touch -- "$vhostDataPath/bootstrap.php" || { appError "Create bootstrap.php failed: $vhostDataPath/bootstrap.php"; return 1; }
|
||||
chown -R -- "$ug:$ug" "$vhostDataPath" || { appError "Change owner of vhost data directory failed: $vhostDataPath"; return 1; }
|
||||
find "$vhostDataPath" -type d -exec chmod 700 -- {} + || { appError "Change permissions of vhost data directories failed"; return 1; }
|
||||
find "$vhostDataPath" -type f -exec chmod 600 -- {} + || { appError "Change permissions of vhost data files failed"; return 1; }
|
||||
}
|
||||
|
||||
# Rebuilds ACL rules for a virtual host.
|
||||
# Resets existing ACLs, then grants OLS nobody user read access to www/data and rw to tmp/session.
|
||||
# $1 (vhostPath): virtual host chroot path.
|
||||
# $2 (vhostDataPath): virtual host data path.
|
||||
function openlitespeedVhostRebuildACL() {
|
||||
local vhostPath="$1"
|
||||
local vhostDataPath="$2"
|
||||
[[ -n "$vhostPath" ]] || { appError "vhostPath not specified"; return 1; }
|
||||
[[ -n "$vhostDataPath" ]] || { appError "vhostDataPath not specified"; return 1; }
|
||||
|
||||
# ACL reset: vhostPath
|
||||
setfacl -R -b -- "$vhostPath" || { appError "Clean ACL rules for vhost path failed: $vhostPath"; return 1; }
|
||||
find "$vhostPath" -type d -exec setfacl -k -- {} + || { appError "Clean default ACL rules for vhost directories failed: $vhostPath"; return 1; }
|
||||
|
||||
# ACL for OLS user nobody: www
|
||||
# Directories: read/traverse + inheritance | Files: read
|
||||
find "$vhostPath/www" -type d -exec setfacl -m u:nobody:rx,m:rx,d:u:nobody:rx,d:m:rx -- {} + || { appError "Set ACL for nobody on www directories failed"; return 1; }
|
||||
find "$vhostPath/www" -type f -exec setfacl -m u:nobody:r,m:r -- {} + || { appError "Set ACL for nobody on www files failed"; return 1; }
|
||||
|
||||
# ACL for OLS user nobody: tmp/session
|
||||
# Directories: rwx + inheritance | Files: rw
|
||||
find "$vhostPath/tmp" "$vhostPath/session" -type d -exec setfacl -m u:nobody:rwx,m:rwx,d:u:nobody:rwx,d:m:rwx -- {} + || { appError "Set ACL for nobody on tmp/session directories failed"; return 1; }
|
||||
find "$vhostPath/tmp" "$vhostPath/session" -type f -exec setfacl -m u:nobody:rw,m:rw -- {} + || { appError "Set ACL for nobody on tmp/session files failed"; return 1; }
|
||||
|
||||
# ACL reset: vhostDataPath
|
||||
setfacl -R -b -- "$vhostDataPath" || { appError "Clean ACL rules for vhost data path failed: $vhostDataPath"; return 1; }
|
||||
find "$vhostDataPath" -type d -exec setfacl -k -- {} + || { appError "Clean default ACL rules for vhost data directories failed: $vhostDataPath"; return 1; }
|
||||
|
||||
# ACL for OLS user nobody: vhostDataPath
|
||||
find "$vhostDataPath" -type d -exec setfacl -m u:nobody:rx,m:rx,d:u:nobody:rx,d:m:rx -- {} + || { appError "Set ACL for nobody on vhost data directories failed"; return 1; }
|
||||
find "$vhostDataPath" -type f -exec setfacl -m u:nobody:r,m:r -- {} + || { appError "Set ACL for nobody on vhost data files failed"; return 1; }
|
||||
}
|
||||
|
||||
# Sets user disk and inode quota for a virtual host.
|
||||
# Requires user quota to be already enabled and active on the target filesystem.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVhostRebuildQuota() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ug
|
||||
ug=$(domainToUser "$domain")
|
||||
|
||||
local quotaMount
|
||||
quotaMount=$(findmnt -no TARGET --target "$vhostsPath")
|
||||
[[ -n "$quotaMount" ]] || { appError "Quota mount not found: $vhostsPath"; return 1; }
|
||||
|
||||
local quotaBlockLimit quotaInodeLimit
|
||||
quotaBlockLimit=$(siteConfigGetOrSet "$domain" "quotaBlockLimit" "$openlitespeedQuotaBlockLimit")
|
||||
quotaInodeLimit=$(siteConfigGetOrSet "$domain" "quotaInodeLimit" "$openlitespeedQuotaInodeLimit")
|
||||
setquota -u "$ug" "$quotaBlockLimit" "$quotaBlockLimit" "$quotaInodeLimit" "$quotaInodeLimit" "$quotaMount" || {
|
||||
appError "Set quota failed: ug=$ug mount=$quotaMount"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Checks whether user quota support is ready on the virtual host filesystem.
|
||||
function openlitespeedQuotaCheck() {
|
||||
local quotaMount
|
||||
quotaMount=$(findmnt -no TARGET --target "$vhostsPath")
|
||||
[[ -n "$quotaMount" ]] || { appError "Quota mount not found: $vhostsPath"; return 1; }
|
||||
|
||||
local quotaOptions
|
||||
quotaOptions=$(findmnt -no OPTIONS --target "$quotaMount")
|
||||
[[ "$quotaOptions" == *usrquota* || "$quotaOptions" == *uquota* ]] || {
|
||||
appError "User quota is not enabled: mount=$quotaMount options=$quotaOptions"
|
||||
return 1
|
||||
}
|
||||
|
||||
quotaon -p "$quotaMount" 2>/dev/null | grep -qi "user quota on" || {
|
||||
appError "User quota is not active: mount=$quotaMount"
|
||||
return 1
|
||||
}
|
||||
|
||||
command -v setquota >/dev/null 2>&1 || { appError "setquota command not found"; return 1; }
|
||||
}
|
||||
|
||||
# Prints disk and inode quota hard limits for a user on the virtual host filesystem.
|
||||
# Output format: <blockLimit> <inodeLimit>
|
||||
# $1 (user): username or numeric UID.
|
||||
function openlitespeedVhostQuota() {
|
||||
local user="$1"
|
||||
[[ -n "$user" ]] || { appError "User not specified"; return 1; }
|
||||
|
||||
local quotaMount
|
||||
quotaMount=$(findmnt -no TARGET --target "$vhostsPath")
|
||||
[[ -n "$quotaMount" ]] || { appError "Quota mount not found: $vhostsPath"; return 1; }
|
||||
|
||||
local quotaLimits error
|
||||
run quotaLimits error quota -u "$user" --filesystem "$quotaMount" || { appError "$error"; return 1; }
|
||||
quotaLimits=$(awk 'NR>2 && $1 != "" { print $4, $7; exit }' <<< "$quotaLimits")
|
||||
[[ -n "$quotaLimits" ]] || { appError "Parse quota limits failed: user=$user mount=$quotaMount"; return 1; }
|
||||
|
||||
printf '%s\n' "$quotaLimits"
|
||||
}
|
||||
|
||||
# Configures XFS project quota for a virtual host.
|
||||
# $1 (vhostPath): virtual host path to assign to an XFS project.
|
||||
# $2 (projectId): numeric XFS project ID.
|
||||
# $3 (projectName): XFS project name.
|
||||
function openlitespeedVhostRebuildXFS() {
|
||||
local vhostPath="$1"
|
||||
local projectId="$2"
|
||||
local projectName="$3"
|
||||
[[ -n "$vhostPath" ]] || { appError "vhostPath not specified"; return 1; }
|
||||
[[ -n "$projectId" ]] || { appError "projectId not specified"; return 1; }
|
||||
[[ -n "$projectName" ]] || { appError "projectName not specified"; return 1; }
|
||||
|
||||
local quotaFs
|
||||
quotaFs=$(findmnt -no FSTYPE --target "$vhostPath")
|
||||
[[ "$quotaFs" == "xfs" ]] || {
|
||||
appError "XFS quota filesystem mismatch: vhostPath=$vhostPath fs=$quotaFs expected=xfs"
|
||||
return 1
|
||||
}
|
||||
|
||||
local quotaMount
|
||||
quotaMount=$(findmnt -no TARGET --target "$vhostPath")
|
||||
[[ -n "$quotaMount" ]] || { appError "Detect XFS quota mount failed: $vhostPath"; return 1; }
|
||||
[[ "$quotaMount" == '/' || "$vhostPath" == "$quotaMount"/* ]] || {
|
||||
appError "XFS quota mount mismatch: vhostPath=$vhostPath quotaMount=$quotaMount expected=$vhostsPath"
|
||||
return 1
|
||||
}
|
||||
|
||||
local quotaOptions
|
||||
quotaOptions=$(findmnt -no OPTIONS --target "$vhostPath")
|
||||
[[ "$quotaOptions" != *noquota* && ( "$quotaOptions" == *prjquota* || "$quotaOptions" == *pquota* ) ]] || {
|
||||
appError "XFS project quota is not enabled: vhostPath=$vhostPath mount=$quotaMount options=$quotaOptions"
|
||||
return 1
|
||||
}
|
||||
|
||||
touch /etc/projects /etc/projid || { appError "Create XFS quota registry files failed"; return 1; }
|
||||
# /etc/projects format: projectId:path
|
||||
sed -i "\#:$vhostPath\$#d" /etc/projects
|
||||
sed -i "\#^$projectId:#d" /etc/projects
|
||||
printf '%s:%s\n' "$projectId" "$vhostPath" >> /etc/projects
|
||||
# /etc/projid format: projectName:projectId
|
||||
sed -i "\#^$projectName:#d" /etc/projid
|
||||
sed -i "\#:$projectId\$#d" /etc/projid
|
||||
printf '%s:%s\n' "$projectName" "$projectId" >> /etc/projid
|
||||
|
||||
xfs_quota -x -c "project -s $projectName" "$quotaMount" || {
|
||||
appError "Set XFS project quota project failed: $projectName $vhostPath"
|
||||
return 1
|
||||
}
|
||||
xfs_quota -x -c "limit -p bhard=$openlitespeedVhostBlockHard ihard=$openlitespeedVhostInodeHard $projectName" "$quotaMount" || {
|
||||
appError "Set XFS project quota limits failed: $projectName"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Rebuilds a virtual host: user/group, filesystem layout, permissions, and ACLs.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVhostRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ug vhostPath vhostDataPath
|
||||
ug=$(domainToUser "$domain")
|
||||
vhostPath="$vhostsPath/$domain"
|
||||
vhostDataPath="$openlitespeedVhostDataPath/$domain"
|
||||
|
||||
# User and group
|
||||
if ! getent group "$ug" >/dev/null 2>&1; then
|
||||
groupadd -- "$ug" || { appError "Create group failed: $ug"; return 1; }
|
||||
fi
|
||||
if ! id "$ug" >/dev/null 2>&1; then
|
||||
useradd -M -g "$ug" -d "$vhostPath" -s /usr/sbin/nologin -- "$ug" >/dev/null 2>&1 || { appError "Create user failed: $ug"; return 1; }
|
||||
else
|
||||
usermod -g "$ug" -d "$vhostPath" -s /usr/sbin/nologin -- "$ug" >/dev/null 2>&1 || { appError "Update user failed: $ug"; return 1; }
|
||||
fi
|
||||
|
||||
openlitespeedVhostRebuildPath "$vhostPath" "$vhostDataPath" "$ug" || return 1
|
||||
openlitespeedVhostRebuildACL "$vhostPath" "$vhostDataPath" || return 1
|
||||
|
||||
# Quota
|
||||
# openlitespeedVhostRebuildQuota "$domain" || return 1
|
||||
|
||||
# XFS [ NO USE ! | Only for XFS + prjquota ]
|
||||
# local uId
|
||||
# uId=$(id -u "$ug" 2>/dev/null)
|
||||
# [[ -n "$uId" ]] || { appError "Get user id failed: $ug"; return 1; }
|
||||
# openlitespeedVhostRebuildXFS "$vhostPath" "$uId" "$domain" || return 1
|
||||
}
|
||||
|
||||
# Creates or deletes OLS config entries for a virtual host.
|
||||
# On create, generates the vhost config file from template if it does not exist.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (option): action: create or delete (defaults to create).
|
||||
function openlitespeedConfigRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local option="${2:-create}"
|
||||
case "$option" in
|
||||
create|delete) ;;
|
||||
*)
|
||||
appError "Unknown option: $option"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
fileBackup "$openlitespeedConfigFile" || return 1
|
||||
|
||||
openlitespeedConfigVHostDel "$domain" || return 1
|
||||
|
||||
local vHostFile="$openlitespeedVhostsPath/$domain.conf"
|
||||
if [[ "$option" == "create" ]]; then
|
||||
openlitespeedConfigEditAdd 'listener\h+HTTP' map "$domain $domain" || return 1
|
||||
openlitespeedConfigVHostAdd "$domain" || return 1
|
||||
|
||||
if [[ ! -f "$vHostFile" ]]; then
|
||||
local profileFile memory_limit max_execution_time post_max_size upload_max_filesize
|
||||
profileFile="$appAssetsPath/openlitespeed/profiles/memory-$kubeMemoryProfile.config"
|
||||
[[ -f "$profileFile" ]] || { appError "Profile not found: $profileFile"; return 1; }
|
||||
memory_limit=$(configGet "$profileFile" "memory_limit")
|
||||
max_execution_time=$(configGet "$profileFile" "max_execution_time")
|
||||
post_max_size=$(configGet "$profileFile" "post_max_size")
|
||||
upload_max_filesize=$(configGet "$profileFile" "upload_max_filesize")
|
||||
|
||||
cp -f -- "$appAssetsPath/openlitespeed/vhost.conf" "$vHostFile" || { appError "Copy vhost template failed"; return 1; }
|
||||
sed -i \
|
||||
-e "s|{{domain}}|$domain|g" \
|
||||
-e "s|{{memory_limit}}|$memory_limit|g" \
|
||||
-e "s|{{max_execution_time}}|$max_execution_time|g" \
|
||||
-e "s|{{post_max_size}}|$post_max_size|g" \
|
||||
-e "s|{{upload_max_filesize}}|$upload_max_filesize|g" \
|
||||
-- "$vHostFile" || { appError "Template substitution failed: $vHostFile"; return 1; }
|
||||
fi
|
||||
else
|
||||
openlitespeedConfigEditDelMasked 'listener\h+HTTP' map "$domain *" || return 1
|
||||
rm -f -- "$vHostFile"
|
||||
rm -f -- "$openlitespeedVhostsPath/$domain.conf0"
|
||||
rm -f -- "$openlitespeedVhostsPath/$domain.txt"
|
||||
openlitespeedConfigEdit vhost_up "$domain"
|
||||
fi
|
||||
|
||||
openlitespeedConfigNormalize "$openlitespeedConfigFile" || return 1
|
||||
}
|
||||
|
||||
# Sets the domain aliases for a virtual host, updating both site config and OLS listener map.
|
||||
# $1 (domain): primary site domain name.
|
||||
# $@ (...): alias domain names to assign.
|
||||
function openlitespeedAliasSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift
|
||||
|
||||
local -a aliasesRaw=("$@")
|
||||
local -a aliases=()
|
||||
|
||||
run vhostList error openlitespeedVhostList || { appError "Failed get list of virtual hosts: $error"; return 1; }
|
||||
listContains "$domain" "$vhostList" || { appError "Domain is not exists in OpenLiteSpeed config"; return 1; }
|
||||
|
||||
for aliasRaw in "${aliasesRaw[@]}"; do
|
||||
alias="$(domainPrepare "$aliasRaw")"
|
||||
[[ -n "$alias" ]] || continue
|
||||
[[ "$alias" == "$domain" ]] && continue
|
||||
arrayContains "$alias" "${aliases[@]}" || aliases+=("$alias")
|
||||
done
|
||||
|
||||
for alias in "${aliases[@]}"; do
|
||||
runError error domainCheck "$alias" || { appError "$alias: $error"; return 1; }
|
||||
listContains "$alias" "$vhostList" && { appError "$alias: Is already exists as virtual host"; return 1; }
|
||||
done
|
||||
|
||||
local aliasValue=''
|
||||
[[ ${#aliases[@]} -gt 0 ]] && aliasValue="$(IFS=','; printf '%s\n' "${aliases[*]}")"
|
||||
|
||||
local domainConfigFile="$appDataPath/config/$domain.config"
|
||||
configSet "$domainConfigFile" alias "$aliasValue" || { appError "Failed set alias in $domainConfigFile"; return 1; }
|
||||
|
||||
fileBackup "$openlitespeedConfigFile" || return 1
|
||||
openlitespeedConfigEditDelMasked 'listener\h+HTTP' map "$domain *" || return 1
|
||||
openlitespeedConfigEditAdd 'listener\h+HTTP' map "$domain $domain" || return 1
|
||||
for alias in "${aliases[@]}"; do
|
||||
openlitespeedConfigEditAdd 'listener\h+HTTP' map "$domain $alias" || return 1
|
||||
done
|
||||
|
||||
printf '%s' "$aliasValue"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Marks a virtual host as suspended.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVHostDown() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local vhostList error
|
||||
run vhostList error openlitespeedVhostList || return 1
|
||||
runSilent fileCheckLineLength "$openlitespeedConfigFile" 8000 || { appError "fileCheckLineLength 8000"; return 1; }
|
||||
if listContains "$domain" "$vhostList"; then
|
||||
openlitespeedConfigEdit vhost_down "$domain" || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Marks a virtual host as active.
|
||||
# $1 (domain): site domain name.
|
||||
function openlitespeedVHostUp() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local vhostList error
|
||||
run vhostList error openlitespeedVhostList || return 1
|
||||
|
||||
if listContains "$domain" "$vhostList"; then
|
||||
openlitespeedConfigEdit vhost_up "$domain" || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Updates the Traefik middleware IP whitelist for the OLS admin panel from $openlitespeedAdminWhiteList.
|
||||
function openlitespeedAdminWhiteList() {
|
||||
local ipList=() whiteList error
|
||||
for i in "${!openlitespeedAdminWhiteList[@]}"; do
|
||||
ipList[$i]="\"${openlitespeedAdminWhiteList[$i]}\""
|
||||
done
|
||||
whiteList=$(IFS=','; printf '%s' "${ipList[*]}")
|
||||
|
||||
runError error k3sRun patch middleware "$openlitespeedKube-admin-allowlist" --type=merge -p "{\"spec\":{\"ipWhiteList\":{\"sourceRange\":[${whiteList}]}}}" \
|
||||
|| { appError "$error"; return 1; }
|
||||
|
||||
printf '%s' "$whiteList"
|
||||
}
|
||||
|
||||
# Restricts OLS admin access to Traefik pod IPs only by updating the admin_config.conf ACL.
|
||||
function openlitespeedAdminAllowList() {
|
||||
local podList
|
||||
podList=$("$k3sCmd" kubectl -n kube-system get pod -l app.kubernetes.io/name=traefik -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}' | awk 'NF')
|
||||
local -a ipList
|
||||
mapfile -t ipList < <(printf '%s\n' "$podList")
|
||||
[[ "${#ipList[@]}" -gt 0 ]] || { appError "Traefik pod IPs not found"; return 1; }
|
||||
|
||||
local allowList
|
||||
allowList=$(IFS=','; printf '%s' "${ipList[*]}")
|
||||
|
||||
fileBackup "$openlitespeedAdminPath/admin_config.conf" || return 1
|
||||
openlitespeedAdminConfigEditSet 'accessControl' deny 'ALL' || return 1
|
||||
openlitespeedAdminConfigEditSet 'accessControl' allow "$allowList" || return 1
|
||||
|
||||
printf '%s' "$allowList"
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
# Executes a command inside the postfix container.
|
||||
function postfixExec() {
|
||||
k3sRun exec -it deploy/"$postfixKube" -c "$postfixKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside a specific Postfix pod.
|
||||
# $1 (pod): pod name.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function postfixPodExec() {
|
||||
local pod="$1"
|
||||
[[ -n "$pod" ]] || { appError "Pod not specified"; return 1; }
|
||||
shift
|
||||
|
||||
k3sRun exec pod/"$pod" -c "$postfixKube" -- "$@"
|
||||
}
|
||||
|
||||
# Converts a domain name to a random 64-char postfix ID.
|
||||
function postfixDomain2id() {
|
||||
domainToRandom "$1" 64
|
||||
}
|
||||
|
||||
# Reloads the Postfix daemon.
|
||||
function postfixReload() {
|
||||
postfixExec postfix reload
|
||||
}
|
||||
|
||||
# Rebuilds lmdb maps for domains, aliases, and senders.
|
||||
function postfixPostmapRebuild() {
|
||||
touch "$postfixConfigPath/$postfixDomainsFile" || return 1
|
||||
touch "$postfixConfigPath/$postfixAliasesFile" || return 1
|
||||
touch "$postfixConfigPath/$postfixSendersFile" || return 1
|
||||
|
||||
local error
|
||||
runError error postfixExec postmap "lmdb:/config/$postfixDomainsFile" || { appError "Failed rebuild $postfixDomainsFile: $error"; return 1; }
|
||||
runError error postfixExec postmap "lmdb:/config/$postfixAliasesFile" || { appError "Failed rebuild $postfixAliasesFile: $error"; return 1; }
|
||||
runError error postfixExec postmap "lmdb:/config/$postfixSendersFile" || { appError "Failed rebuild $postfixSendersFile: $error"; return 1; }
|
||||
}
|
||||
|
||||
# Sets or removes a key/value in a postfix map file.
|
||||
# $1 (file): path to the map file.
|
||||
# $2 (key): map key.
|
||||
# [$3] (value): map value; omit to delete the key.
|
||||
function postfixConfigSet() {
|
||||
local file="$1" key="$2" value="$3"
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
[[ -n "$key" ]] || { appError "Key not specified"; return 1; }
|
||||
|
||||
value="${value//$'\r'/ }"
|
||||
value="${value//$'\n'/ }"
|
||||
local output error
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
mkdir -p "$(dirname -- "$file")" || { appError "Failed to create folder"; return 1; }
|
||||
install -o root -g root -m 644 /dev/null "$file" || { appError "Failed to create file"; return 1; }
|
||||
else
|
||||
runError error sed -i -E "/^[[:space:]]*${key}[[:space:]]+/d" "$file" || { appError "Error writing to a file: $error"; return 1; }
|
||||
fi
|
||||
|
||||
if [[ -n "$value" ]]; then
|
||||
runShell output error printf "%s\n" "$key $value" ">>" "$file" || { appError "Error writing to a file: $error"; return 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
# Sets or removes a domain entry in the virtual domains map.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (value): map value; omit to delete.
|
||||
function postfixVirtualDomainSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
postfixConfigSet "$postfixConfigPath/$postfixDomainsFile" "$domain" "$2"
|
||||
}
|
||||
|
||||
# Sets or removes an entry in the virtual aliases map.
|
||||
function postfixVirtualAliasSet() {
|
||||
postfixConfigSet "$postfixConfigPath/$postfixAliasesFile" "$@"
|
||||
}
|
||||
|
||||
# Sets or removes a domain entry in the virtual domains map (lmdb-safe variant).
|
||||
# $1 (key): map key.
|
||||
# [$2] (value): map value; omit to delete.
|
||||
function postfixVirtualDomainSet2() {
|
||||
local key="$1"
|
||||
[[ -n "$key" ]] || { appError "Key not specified"; return 1; }
|
||||
local value="$2"
|
||||
local file="$postfixConfigPath/$postfixDomainsFile"
|
||||
|
||||
local escaped
|
||||
escaped=$(printf '%s\n' "$key" | sed 's/[.[\*^$()+?{}|\\/]/\\&/g')
|
||||
sed -i "/^${escaped}[[:space:]]/d" "$file" || { appError "Failed to clean old key in $file"; return 1; }
|
||||
|
||||
if [[ -n "$value" ]]; then
|
||||
printf '%s %s\n' "$key" "$value" >> "$file" || { appError "Failed to append to $file"; return 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
# Adds or removes a (sender, login) pair from the owners file and rebuilds senders map.
|
||||
# $1 (sender): sender address.
|
||||
# $2 (login): SASL login.
|
||||
# [$3] (save): non-empty to add; omit to remove.
|
||||
function postfixSenderOwnerSet() {
|
||||
local sender="$1" login="$2" save="$3"
|
||||
[[ -n "$sender" ]] || { appError "Sender not set"; return 1; }
|
||||
[[ -n "$login" ]] || { appError "Login not set"; return 1; }
|
||||
local ownersFile="$postfixConfigPath/$postfixSendersFile.owners"
|
||||
touch "$ownersFile" || return 1
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
awk -v s="$sender" -v l="$login" '!(NF>=2 && $1==s && $2==l)' "$ownersFile" >"$tmp"
|
||||
if [[ -n "$save" ]]; then
|
||||
printf '%s %s\n' "$sender" "$login" >>"$tmp"
|
||||
fi
|
||||
mv -f "$tmp" "$ownersFile"
|
||||
|
||||
postfixSendersRebuild
|
||||
}
|
||||
|
||||
# Sets or removes an entry in the virtual aliases map (lmdb-safe variant).
|
||||
# $1 (key): map key.
|
||||
# [$2] (value): map value; omit to delete.
|
||||
function postfixVirtualAliasSet2() {
|
||||
local key="$1"
|
||||
[[ -n "$key" ]] || { appError "Key not specified"; return 1; }
|
||||
local value="$2"
|
||||
local file="$postfixConfigPath/$postfixAliasesFile"
|
||||
|
||||
local escaped
|
||||
escaped=$(printf '%s\n' "$key" | sed 's/[.[\*^$()+?{}|\\/]/\\&/g')
|
||||
sed -i "/^${escaped}[[:space:]]/d" "$file" || { appError "Failed to clean old key in $file"; return 1; }
|
||||
|
||||
if [[ -n "$value" ]]; then
|
||||
printf '%s %s\n' "$key" "$value" >> "$file" || { appError "Failed to append to $file"; return 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
# Rebuilds the senders map from the .owners file, deduplicating per sender.
|
||||
function postfixSendersRebuild() {
|
||||
local ownersFile="$postfixConfigPath/$postfixSendersFile.owners"
|
||||
local outFile="$postfixConfigPath/$postfixSendersFile"
|
||||
|
||||
touch "$ownersFile" || return 1
|
||||
|
||||
local tmpNorm tmpOut
|
||||
tmpNorm=$(mktemp)
|
||||
tmpOut=$(mktemp)
|
||||
|
||||
awk 'NF>=2 && $1 !~ /^#/ { print $1, $2 }' "$ownersFile" >"$tmpNorm"
|
||||
|
||||
awk '
|
||||
{ s=$1; o=$2; k=s SUBSEP o;
|
||||
if (!seen[k]++) {
|
||||
if (list[s]=="") list[s]=o; else list[s]=list[s] "," o;
|
||||
if (!sseen[s]++) order[++n]=s;
|
||||
}
|
||||
}
|
||||
END { for (i=1; i<=n; i++) { s=order[i]; print s " " list[s]; } }
|
||||
' "$tmpNorm" >"$tmpOut"
|
||||
|
||||
mv -f "$tmpOut" "$outFile"
|
||||
rm -f "$tmpNorm" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Creates, updates, or deletes a Postfix SASL user.
|
||||
# $1 (login): SASL username.
|
||||
# [$2] (password): password; omit to delete the user.
|
||||
function postfixSaslUserSet() {
|
||||
local login="$1" password="$2"
|
||||
[[ -n "$login" ]] || { appError "Login not specified"; return 1; }
|
||||
|
||||
local error
|
||||
if [[ -z "$password" ]]; then
|
||||
runError error postfixExec saslpasswd2 -d -f "/config/sasldb2" -u "$postfixDefaultRealm" "$login" || {
|
||||
appError "Failed to delete SASL user $login"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
runError error postfixExec \
|
||||
env SASL_LOGIN="$login" SASL_PWD="$password" SASL_DB="/config/sasldb2" SASL_REALM="$postfixDefaultRealm" \
|
||||
sh -lc 'printf "%s\n" "$SASL_PWD" | saslpasswd2 -p -c -f "$SASL_DB" -u "$SASL_REALM" "$SASL_LOGIN"' || {
|
||||
appError "Failed to create/update SASL user $login: $error"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# Lists all domains from the virtual domains map file.
|
||||
function postfixDomainList() {
|
||||
local file="$postfixConfigPath/$postfixDomainsFile"
|
||||
[[ -f "$file" ]] || { appError "Domains file not found: $file"; return 1; }
|
||||
|
||||
awk '
|
||||
/^[[:space:]]*$/ { next }
|
||||
/^[[:space:]]*#/ { next }
|
||||
{ print $1 }
|
||||
' "$file" | sort -u
|
||||
}
|
||||
|
||||
# Registers a domain in Postfix: creates SASL user, sets sender ownership, optionally adds alias.
|
||||
# $1 (domain): site domain name.
|
||||
function postfixDomainAdd() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
local pfxId
|
||||
pfxId=$(postfixDomain2id "$domain")
|
||||
local postfixUser postfixPass
|
||||
postfixUser=$(siteConfigGetOrSet "$domain" "postfixUser" "$pfxId")
|
||||
postfixPass=$(siteConfigGetOrCreate "$domain" "postfixPass")
|
||||
|
||||
postfixSenderOwnerSet "$postfixPostmaster" "$postfixUser@$postfixDefaultRealm" "SAVE" || return 1
|
||||
postfixSaslUserSet "$postfixUser" "$postfixPass" || return 1
|
||||
|
||||
local postfixForwardTo
|
||||
postfixForwardTo=$(siteConfigGet "$domain" "postfixForwardTo")
|
||||
if [[ -n "$postfixForwardTo" ]]; then
|
||||
postfixVirtualAliasSet "@$domain" "$postfixForwardTo"
|
||||
postfixVirtualDomainSet "$domain" "OK" || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Removes a domain from Postfix: clears SASL user, sender ownership, alias, and domain entry.
|
||||
# $1 (domain): site domain name.
|
||||
function postfixDomainRemove() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
local postfixUser
|
||||
postfixUser=$(siteConfigGet "$domain" "postfixUser")
|
||||
[[ -n "$postfixUser" ]] || { appError "postfixUser not found"; return 1; }
|
||||
|
||||
postfixVirtualDomainSet "$domain"
|
||||
postfixSenderOwnerSet "$postfixPostmaster" "$postfixUser@$postfixDefaultRealm"
|
||||
postfixSaslUserSet "$postfixUser"
|
||||
postfixVirtualAliasSet "@$domain"
|
||||
}
|
||||
|
||||
# Registers a domain in Postfix and rebuilds lmdb maps.
|
||||
# $1 (domain): site domain name.
|
||||
function postfixConfigRebuild() {
|
||||
local domain error
|
||||
domain=$(domainPrepare "$1")
|
||||
runError error domainCheck "$domain" || { appError "$error"; return 1; }
|
||||
runError error postfixDomainAdd "$domain" || { appError "$error"; return 1; }
|
||||
postfixPostmapRebuild
|
||||
}
|
||||
|
||||
# Reloads the opendkim daemon.
|
||||
function postfixDkimReload() {
|
||||
postfixExec sh -lc "pkill -HUP -f '/usr/sbin/opendkim' 2>/dev/null"
|
||||
}
|
||||
|
||||
# Lists domains that have DKIM key material present.
|
||||
function postfixDkimList() {
|
||||
ls -1 "$postfixDkimPath"/keys/*.txt 2>/dev/null | xargs -n1 basename | sed "s/\.txt$//" || true
|
||||
}
|
||||
|
||||
# Generates a DKIM keypair for a domain and updates SigningTable/KeyTable.
|
||||
# $1 (domain): domain name.
|
||||
function postfixDkimAdd() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
if ! test -s "$postfixDkimPath/keys/$domain.private" || ! test -s "$postfixDkimPath/keys/$domain.txt"; then
|
||||
postfixExec sh -lc "
|
||||
set -e
|
||||
|
||||
mkdir -p /etc/opendkim/keys
|
||||
touch /etc/opendkim/SigningTable /etc/opendkim/KeyTable
|
||||
|
||||
opendkim-genkey -b 2048 -r -D '/etc/opendkim/keys' -d '$domain' -s '$postfixDkimSelector'
|
||||
mv -f \"/etc/opendkim/keys/$postfixDkimSelector.private\" \"/etc/opendkim/keys/$domain.private\"
|
||||
mv -f \"/etc/opendkim/keys/$postfixDkimSelector.txt\" \"/etc/opendkim/keys/$domain.txt\"
|
||||
|
||||
chown opendkim:opendkim \"/etc/opendkim/keys/$domain.private\" \"/etc/opendkim/keys/$domain.txt\" || true
|
||||
chmod 0600 \"/etc/opendkim/keys/$domain.private\"
|
||||
chmod 0644 \"/etc/opendkim/keys/$domain.txt\"
|
||||
" || { appError "Failed to add DKIM for $domain"; return 1; }
|
||||
fi
|
||||
|
||||
sed -i "/^\*@$domain[[:space:]]/d" "$postfixDkimPath/SigningTable"
|
||||
sed -i "/^$postfixDkimSelector\._domainkey\.$domain[[:space:]]/d" "$postfixDkimPath/KeyTable"
|
||||
|
||||
echo "*@$domain $postfixDkimSelector._domainkey.$domain" >> "$postfixDkimPath/SigningTable"
|
||||
echo "$postfixDkimSelector._domainkey.$domain $domain:$postfixDkimSelector:/etc/opendkim/keys/$domain.private" >> "$postfixDkimPath/KeyTable"
|
||||
|
||||
postfixDkimReload
|
||||
}
|
||||
|
||||
# Autocreates DKIM keys if missing and returns the TXT record, or lists available domains.
|
||||
# [$1] (domain): domain name; omit to list all domains.
|
||||
function postfixDkimGet() {
|
||||
local domain="$1"
|
||||
if [[ -z "$domain" ]]; then
|
||||
postfixDkimList
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! "$domain" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]]; then
|
||||
appError "Invalid domain: $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
postfixDkimAdd "$domain" || { appError "Failed to create DKIM for $domain"; return 1; }
|
||||
cat "$postfixDkimPath/keys/$domain.txt" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Removes DKIM key material for a domain and reloads opendkim.
|
||||
# $1 (domain): domain name.
|
||||
function postfixDkimRemove() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
sed -i "/^\*@$domain[[:space:]]/d" "$postfixDkimPath/SigningTable"
|
||||
sed -i "/^$postfixDkimSelector\._domainkey\.$domain[[:space:]]/d" "$postfixDkimPath/KeyTable"
|
||||
|
||||
postfixDkimReload
|
||||
}
|
||||
|
||||
# Sends mail through Postfix from a raw RFC822 message file.
|
||||
# $1 (mailFrom): envelope sender address.
|
||||
# $2 (msgFile): path to the RFC822 message file.
|
||||
function postfixMailSendFromFile() {
|
||||
local mailFrom="$1" msgFile="$2"
|
||||
[[ -n "$mailFrom" ]] || { appError "Mail not specified"; return 1; }
|
||||
[[ -n "$msgFile" ]] || { appError "File not specified"; return 1; }
|
||||
|
||||
local output error
|
||||
runShell output error postfixExec sh -lc "sendmail -v -oi -t -f '$mailFrom'" "<" "$msgFile" || {
|
||||
[[ -n "$error" ]] && appError "$error"
|
||||
return 1
|
||||
}
|
||||
printf '%s' "$output"
|
||||
}
|
||||
|
||||
# Lists aliases from the virtual aliases map file.
|
||||
function postfixAliasList() {
|
||||
local file="$postfixConfigPath/$postfixAliasesFile"
|
||||
[[ -f "$file" ]] || { appError "Aliases file not found: $file"; return 1; }
|
||||
|
||||
awk '
|
||||
/^[[:space:]]*$/ { next }
|
||||
/^[[:space:]]*#/ { next }
|
||||
{ print $1 " -> " $2 }
|
||||
' "$file" | sort -u
|
||||
}
|
||||
|
||||
# Lists senders from the senders owners file.
|
||||
function postfixSendersList() {
|
||||
local file="$postfixConfigPath/$postfixSendersFile.owners"
|
||||
[[ -f "$file" ]] || { appError "Senders file not found: $file"; return 1; }
|
||||
|
||||
awk '
|
||||
/^[[:space:]]*$/ { next }
|
||||
/^[[:space:]]*#/ { next }
|
||||
{ print $1 " -> " $2 }
|
||||
' "$file" | sort -u
|
||||
}
|
||||
|
||||
# Lists all SASL users from the sasldb2 database.
|
||||
function postfixSaslUserList() {
|
||||
local output error
|
||||
run output error postfixExec sasldblistusers2 -f /config/sasldb2 || { appError "$error"; return 1; }
|
||||
|
||||
awk -F':' '
|
||||
/^[[:space:]]*$/ { next }
|
||||
{ gsub(/[[:space:]]+$/, "", $1); print $1 }
|
||||
' <<<"$output"
|
||||
}
|
||||
|
||||
# Returns the value for a key in a postfix map file; exits non-zero if not found.
|
||||
# $1 (file): path to the map file.
|
||||
# $2 (key): map key to look up.
|
||||
function postfixMapHas() {
|
||||
local file="$1" key="$2"
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
[[ -f "$file" ]] || { appError "File not found"; return 1; }
|
||||
[[ -n "$key" ]] || { appError "Key not specified"; return 1; }
|
||||
|
||||
awk -v k="$key" 'NF>=2 && $1==k {print $2; found=1} END {exit !found}' "$file"
|
||||
}
|
||||
|
||||
# Checks whether (postmaster, login) pair exists in the senders owners file.
|
||||
# $1 (login): SASL login to look up.
|
||||
function postfixSenderOwnerHas() {
|
||||
local login="$1"
|
||||
[[ -n "$login" ]] || { appError "Login not specified"; return 1; }
|
||||
local file="$postfixConfigPath/$postfixSendersFile.owners"
|
||||
[[ -f "$file" ]] || { appError "File not found"; return 1; }
|
||||
|
||||
awk -v s="$postfixPostmaster" -v l="$login" 'NF>=2 && $1==s && $2==l {found=1} END{exit !found}' "$file"
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
# Executes a command inside the Redis master pod.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function redisExec() {
|
||||
k3sRun exec pod/"$redisKube"-0 -c "$redisKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside a specific Redis or Redis Sentinel pod.
|
||||
# Container is selected automatically based on the pod name prefix.
|
||||
# $1 (pod): pod name.
|
||||
# $2+ (...): command and arguments to execute.
|
||||
function redisPodExec() {
|
||||
local pod="$1"
|
||||
[[ -n "$pod" ]] || { appError "Pod not specified"; return 1; }
|
||||
shift
|
||||
|
||||
local container="$redisKube"
|
||||
[[ "$pod" == "$redisSentinelKube-"* ]] && container="$redisSentinelKube"
|
||||
|
||||
k3sRun exec pod/"$pod" -c "$container" -- "$@"
|
||||
}
|
||||
|
||||
# Runs redis-cli against the master pod, with authentication if configured.
|
||||
# $@ (...): redis-cli arguments.
|
||||
function redisExecCli() {
|
||||
local -a cmd=(redis-cli --no-auth-warning)
|
||||
[[ -n "$redisRootPass" ]] && cmd+=(-a "$redisRootPass")
|
||||
redisExec "${cmd[@]}" "$@"
|
||||
}
|
||||
|
||||
# Runs redis-cli on a specific pod, with authentication and port selected automatically.
|
||||
# Uses port 26379 for Sentinel pods.
|
||||
# $1 (pod): pod name.
|
||||
# $2+ (...): redis-cli arguments.
|
||||
function redisPodExecCli() {
|
||||
local pod="$1"
|
||||
shift
|
||||
|
||||
local -a cmd=(redis-cli --no-auth-warning)
|
||||
[[ -n "$redisRootPass" ]] && cmd+=(-a "$redisRootPass")
|
||||
[[ "$pod" == "$redisSentinelKube-"* ]] && cmd+=(-p 26379)
|
||||
|
||||
redisPodExec "$pod" "${cmd[@]}" "$@"
|
||||
}
|
||||
|
||||
# Converts a domain name into a Redis-safe identifier (max 64 chars).
|
||||
# $1 (domain): domain name.
|
||||
function redisDomain2id() {
|
||||
domainToRandom "$1" 64
|
||||
}
|
||||
|
||||
# Reads the Redis root password from the Kubernetes secret.
|
||||
function redisRootPassSecretGet() {
|
||||
k3sRun get secret "$redisKube-secret" -o jsonpath="{.data.root-password}" --ignore-not-found | base64 -d
|
||||
}
|
||||
|
||||
# Saves the Redis root password from the Kubernetes secret to a local file.
|
||||
function redisRootPassSecretSave() {
|
||||
local password
|
||||
password="$(redisRootPassSecretGet)"
|
||||
[[ -n "$password" ]] && printf '%s\n' "$password" > "$appDataPath/$hostName.$redisKube"
|
||||
}
|
||||
|
||||
# Updates the Redis root password across pods. Not yet implemented.
|
||||
function redisRootPassUpdate() {
|
||||
|
||||
printWarning "In progress..."
|
||||
|
||||
# Add update pass in RedisSentinel and HAProxy !!!...
|
||||
|
||||
# k3sRun create secret generic "$redisKube-secret" --from-literal=root-password="$redisRootPass" --dry-run=client -o yaml | k3sRun apply -f -
|
||||
# k3sRun delete pod "$redisKube-0"
|
||||
# sleep 1
|
||||
# k3sRun delete pod "$redisKube-1"
|
||||
# k3sRun rollout restart "sts/$redisSentinelKube"
|
||||
|
||||
# return 1
|
||||
}
|
||||
|
||||
# List Redis users via ACL LIST (names only).
|
||||
function redisUserListGet() {
|
||||
local output error
|
||||
run output error redisExecCli ACL LIST || { appError "$error"; return 1; }
|
||||
printf '%s' "$output" | grep -oP 'user \K\w+'
|
||||
}
|
||||
|
||||
# Flushes all keys from the current Redis database.
|
||||
function redisDatabaseFlush() {
|
||||
runFail redisExecCli FLUSHDB
|
||||
}
|
||||
|
||||
# Persists the ACL user list to disk.
|
||||
function redisUserListSave() {
|
||||
runFail redisExecCli ACL SAVE
|
||||
}
|
||||
|
||||
# Creates or updates a Redis ACL user with password and key pattern.
|
||||
# $1 (username): ACL username.
|
||||
# $2 (password): ACL password.
|
||||
# [$3] (keyPattern): key access pattern (defaults to "username:*").
|
||||
function redisUserSet() {
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$2"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
local keyPattern="${3:-${username}:*}"
|
||||
|
||||
local podList error
|
||||
run podList error k3sPodList "$redisKube" || { appError "Get pods list: $error"; return 1; }
|
||||
[[ -n "$podList" ]] || { appError "Pods not found"; return 1; }
|
||||
|
||||
while read -r pod; do
|
||||
runFail redisPodExecCli "$pod" ACL SETUSER "$username" reset on sanitize-payload resetchannels ">$password" "~$keyPattern" -@all +@connection +@string +@keyspace +@sortedset +@scripting +@transaction +info -keys -flushdb -flushall '-script|flush' '-client|kill' '-client|pause' || return 1
|
||||
runFail redisPodExecCli "$pod" ACL SAVE || return 1
|
||||
done <<< "$podList"
|
||||
}
|
||||
|
||||
# Removes a Redis ACL user from all pods.
|
||||
# $1 (username): ACL username.
|
||||
function redisUserRemove() {
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
|
||||
local podList error
|
||||
run podList error k3sPodList "$redisKube" || { appError "Get pods list: $error"; return 1; }
|
||||
[[ -n "$podList" ]] || { appError "Pods not found"; return 1; }
|
||||
|
||||
while read -r pod; do
|
||||
runFail redisPodExecCli "$pod" ACL DELUSER "$username" || return 1
|
||||
runFail redisPodExecCli "$pod" ACL SAVE || return 1
|
||||
done <<< "$podList"
|
||||
}
|
||||
|
||||
# Deletes all Redis keys matching the given prefix.
|
||||
# $1 (prefixKey): key prefix to match (e.g. "user:").
|
||||
function redisKeysRemove() {
|
||||
local prefixKey="$1"
|
||||
[[ -n "$prefixKey" ]] || { appError "PrefixKey not specified"; return 1; }
|
||||
|
||||
local output error
|
||||
run output error redisExecCli --scan --pattern "${prefixKey}*" || { appError "$error"; return 1; }
|
||||
[[ -z "$output" ]] && return 0
|
||||
|
||||
local -a keys
|
||||
mapfile -t keys <<< "$output"
|
||||
runFail redisExecCli DEL "${keys[@]}"
|
||||
}
|
||||
|
||||
# Removes all Redis keys belonging to a site's user.
|
||||
# $1 (domain): site domain name.
|
||||
function redisDomainClean() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local redisUser
|
||||
redisUser=$(siteConfigGet "$domain" "redisUser")
|
||||
[[ -n "$redisUser" ]] && redisKeysRemove "$redisUser:"
|
||||
}
|
||||
|
||||
# Creates or updates the Redis ACL user and key pattern for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function redisConfigRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
fileBackup "$redisPath/$redisFileUsersAcl"
|
||||
|
||||
local redisUser redisPass
|
||||
redisUser=$(siteConfigGetOrSet "$domain" "redisUser" "$(redisDomain2id "$domain")")
|
||||
redisPass=$(siteConfigGetOrCreate "$domain" "redisPass")
|
||||
redisUserSet "$redisUser" "$redisPass" || return 1
|
||||
}
|
||||
|
||||
# Parses raw SENTINEL REPLICAS output into tab-separated lines (name, ip, port, master-host, runid).
|
||||
# Skips disconnected/s_down replicas and deduplicates by runid.
|
||||
# $1 (raw): raw output from `SENTINEL replicas <master>`.
|
||||
function redisSentinelParseReplicas() {
|
||||
local raw="$1"
|
||||
local key value flags
|
||||
local -A slaveData=()
|
||||
local -A seenRunIds=()
|
||||
|
||||
while read -r key && read -r value; do
|
||||
if [[ "$key" == "name" && ${#slaveData[@]} -gt 0 ]]; then
|
||||
flags="${slaveData[flags]}"
|
||||
if [[ -n "${slaveData[runid]}" && "$flags" != *disconnected* && "$flags" != *s_down* ]]; then
|
||||
if [[ -z "${seenRunIds[${slaveData[runid]}]}" ]]; then
|
||||
seenRunIds["${slaveData[runid]}"]=1
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "${slaveData[name]}" "${slaveData[ip]}" "${slaveData[port]}" "${slaveData[master-host]}" "${slaveData[runid]}"
|
||||
fi
|
||||
fi
|
||||
slaveData=()
|
||||
fi
|
||||
slaveData["$key"]="$value"
|
||||
done <<< "$raw"
|
||||
|
||||
if [[ ${#slaveData[@]} -gt 0 ]]; then
|
||||
flags="${slaveData[flags]}"
|
||||
if [[ -n "${slaveData[runid]}" && "$flags" != *disconnected* && "$flags" != *s_down* ]]; then
|
||||
if [[ -z "${seenRunIds[${slaveData[runid]}]}" ]]; then
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "${slaveData[name]}" "${slaveData[ip]}" "${slaveData[port]}" "${slaveData[master-host]}" "${slaveData[runid]}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
# Sends a text message to Rocket.Chat via webhook.
|
||||
# Uses global $rocketChatUrl.
|
||||
# $1 (text): message text
|
||||
# [$2] (attachFile): optional path to a file to attach
|
||||
function rocketChatSend() {
|
||||
[[ -n "${rocketChatUrl:-}" ]] || { appError "rocketChatUrl is not configured"; return 1; }
|
||||
|
||||
local text="$1"
|
||||
local attachFile="${2:-}"
|
||||
|
||||
local payload
|
||||
if [[ -n "$attachFile" ]]; then
|
||||
local attachText
|
||||
if [[ -f "$attachFile" ]]; then
|
||||
attachText=$(< "$attachFile")
|
||||
else
|
||||
attachText="_(file not found)_"
|
||||
fi
|
||||
payload=$(jq -nc \
|
||||
--arg text "$text" \
|
||||
--arg title "$attachFile" \
|
||||
--arg attachText "$attachText" \
|
||||
'{"text": $text, "attachments": [{"title": $title, "text": ("```shell\n" + $attachText + "\n```"), "collapsed": true}]}')
|
||||
else
|
||||
payload=$(jq -nc --arg text "$text" '{"text": $text}')
|
||||
fi
|
||||
|
||||
local error
|
||||
runError error curl -sf -X POST -H "Content-Type: application/json" --data "$payload" --retry 2 --retry-delay 1 --max-time 10 -- "$rocketChatUrl" || {
|
||||
appError "Failed to send Rocket.Chat message${error:+: $error}";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
# Formats a diagnostic report message for Rocket.Chat and outputs it to stdout.
|
||||
# Uses globals: $hostName.
|
||||
# $1 (status): report status (uppercased automatically)
|
||||
# $2 (report): report text (plain multiline string)
|
||||
function rocketChatFormatReport() {
|
||||
local status="${1^^}"
|
||||
local report="$2"
|
||||
|
||||
local -a lines=(
|
||||
"🚨 *KUBE Alert: $status*"
|
||||
""
|
||||
"*Source:* \`$hostName\`"
|
||||
"*Generated at:* \`$(TZ='Europe/Prague' date '+%Y-%m-%d %H:%M:%S') (Europe/Prague)\`"
|
||||
""
|
||||
"$report"
|
||||
)
|
||||
|
||||
printf '%s\n' "${lines[@]}"
|
||||
}
|
||||
|
||||
# Sends a text message to Rocket.Chat via webhook.
|
||||
# Uses global $rocketChatUrl.
|
||||
# $1 (text): message text
|
||||
# [$2] (attachFile): optional path to a file to attach
|
||||
function rocketChatSend2() {
|
||||
[[ -n "${rocketChatUrl:-}" ]] || { appError "rocketChatUrl is not configured"; return 1; }
|
||||
|
||||
local text="$1"
|
||||
local attachFile="${2:-}"
|
||||
|
||||
local payload
|
||||
if [[ -n "$attachFile" ]]; then
|
||||
local attachText
|
||||
if [[ -f "$attachFile" ]]; then
|
||||
attachText=$(< "$attachFile")
|
||||
else
|
||||
attachText="_(file not found)_"
|
||||
fi
|
||||
|
||||
payload=$(jq -nc \
|
||||
--arg text "$text" \
|
||||
--arg title "$attachFile" \
|
||||
--arg attachText "$attachText" \
|
||||
'{"text": ($text + "\n\n<details>\n<summary>📄 " + $title + " (Click to expand)</summary>\n\n```shell\n" + $attachText + "\n```\n</details>")}')
|
||||
else
|
||||
payload=$(jq -nc --arg text "$text" '{"text": $text}')
|
||||
fi
|
||||
|
||||
local error
|
||||
runError error curl -sf -X POST -H "Content-Type: application/json" --data "$payload" --retry 2 --retry-delay 1 --max-time 10 -- "$rocketChatUrl" || {
|
||||
appError "Failed to send Rocket.Chat message${error:+: $error}";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
# Reads a value from the site config file.
|
||||
# $1 (domain): site domain name.
|
||||
# $@ (...): arguments passed to configGet.
|
||||
function siteConfigGet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift
|
||||
|
||||
configGet "$appDataPath/config/$domain.config" "$@"
|
||||
}
|
||||
|
||||
# Writes a value to the site config file.
|
||||
# $1 (domain): site domain name.
|
||||
# $@ (...): arguments passed to configSet.
|
||||
function siteConfigSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift
|
||||
|
||||
configSet "$appDataPath/config/$domain.config" "$@"
|
||||
}
|
||||
|
||||
# Reads or sets a default value in the site config file.
|
||||
# $1 (domain): site domain name.
|
||||
# $@ (...): arguments passed to configGetOrSet.
|
||||
function siteConfigGetOrSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift
|
||||
|
||||
configGetOrSet "$appDataPath/config/$domain.config" "$@"
|
||||
}
|
||||
|
||||
# Reads or generates a new value in the site config file.
|
||||
# $1 (domain): site domain name.
|
||||
# $@ (...): arguments passed to configGetOrCreate.
|
||||
function siteConfigGetOrCreate() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
shift
|
||||
|
||||
configGetOrCreate "$appDataPath/config/$domain.config" "$@"
|
||||
}
|
||||
|
||||
# Creates a new site: validates uniqueness, provisions OLS/MariaDB/Redis/Postfix, and imports files and DB.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (sourceFiles): path to the site files directory or archive.
|
||||
# [$3] (sourceDatabase): path to a SQL dump file to import (optional).
|
||||
function siteAdd() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local sourceFiles="$2"
|
||||
[[ -n "$sourceFiles" ]] || { appError "Source files not specified"; return 1; }
|
||||
[[ -e "$sourceFiles" ]] || { appError "Source files not found: $sourceFiles"; return 1; }
|
||||
|
||||
local sourceDatabase="$3"
|
||||
if [[ -n "$sourceDatabase" && ! -f "$sourceDatabase" ]]; then
|
||||
appError "Source database not found: $sourceDatabase"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local targetPath="$vhostsPath/$domain"
|
||||
[[ ! -e "$targetPath" ]] || { appError "The directory or file exists: $targetPath"; return 1; }
|
||||
|
||||
# OpenLiteSpeed
|
||||
local vhostList aliasList error
|
||||
if ! run vhostList error openlitespeedVhostList all; then
|
||||
appError "Error retrieving vhost list: $error"
|
||||
return 1
|
||||
elif listContains "$domain" "$vhostList"; then
|
||||
appError "Domain already exists in OpenLiteSpeed config: $domain"
|
||||
return 1
|
||||
fi
|
||||
if ! run aliasList error openlitespeedAliasList; then
|
||||
appError "Error retrieving alias list: $error"
|
||||
return 1
|
||||
elif listContains "$domain" "$aliasList"; then
|
||||
appError "Domain already exists in OpenLiteSpeed config (alias): $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# MariaDB
|
||||
local databaseName databaseUser databaseNameList databaseUserList
|
||||
databaseName=$(siteConfigGetOrSet "$domain" "databaseName" "$(mariadbDomain2id "$domain")")
|
||||
databaseUser=$(siteConfigGetOrSet "$domain" "databaseUser" "$(mariadbDomain2id "$domain")")
|
||||
if ! run databaseNameList error mariadbDatabaseListGet; then
|
||||
appError "Error retrieving MariaDB database list: $error"
|
||||
return 1
|
||||
elif listContains "$databaseName" "$databaseNameList"; then
|
||||
appError "Database already exists in MariaDB: $databaseName"
|
||||
return 1
|
||||
fi
|
||||
if ! run databaseUserList error mariadbUserListGet; then
|
||||
appError "Error retrieving MariaDB user list: $error"
|
||||
return 1
|
||||
elif listContains "$databaseUser" "$databaseUserList"; then
|
||||
appError "User already exists in MariaDB: $databaseUser"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Redis
|
||||
local redisUser redisUserList
|
||||
redisUser=$(siteConfigGetOrSet "$domain" "redisUser" "$(redisDomain2id "$domain")")
|
||||
if ! run redisUserList error redisUserListGet; then
|
||||
appError "Error retrieving Redis user list: $error"
|
||||
return 1
|
||||
elif listContains "$redisUser" "$redisUserList"; then
|
||||
appError "User already exists in Redis: $redisUser"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Postfix
|
||||
# TODO
|
||||
# TODO Check site in Postfix
|
||||
# TODO
|
||||
|
||||
# Preparing and verifying the file source
|
||||
local tmpFiles=""
|
||||
local importFiles output rc
|
||||
if [[ -f "$sourceFiles" ]]; then
|
||||
tmpFiles=$(mktemp -d) || {
|
||||
appError "Failed to create temporary restore directory"
|
||||
return 1
|
||||
}
|
||||
|
||||
archiveExtract "$sourceFiles" "$tmpFiles" || {
|
||||
rm -rf -- "$tmpFiles" &>/dev/null
|
||||
return 1
|
||||
}
|
||||
|
||||
importFiles="$tmpFiles"
|
||||
else
|
||||
importFiles="$sourceFiles"
|
||||
fi
|
||||
|
||||
# Preparing and Verifying the Database Source
|
||||
if [[ -n "$sourceDatabase" ]]; then
|
||||
local importDbName importDbUser importDbPass
|
||||
importDbName=$(uuidgen) || return 1
|
||||
importDbUser=$(uuidgen) || return 1
|
||||
importDbPass=$(uuidgen) || return 1
|
||||
if ! runError error mariadbDatabaseUserSet "$importDbName" "$importDbUser" "$importDbPass"; then
|
||||
mariadbDatabaseRemove "$importDbName"
|
||||
mariadbUserRemove "$importDbUser"
|
||||
[[ -n "$tmpFiles" ]] && rm -rf -- "$tmpFiles" &>/dev/null
|
||||
appError "Failed to prepare tmp database"
|
||||
return 1
|
||||
fi
|
||||
if ! run output error mariadbMasterImport "$importDbName" "$importDbUser" "$importDbPass" "$sourceDatabase"; then
|
||||
mariadbDatabaseRemove "$importDbName"
|
||||
mariadbUserRemove "$importDbUser"
|
||||
[[ -n "$tmpFiles" ]] && rm -rf -- "$tmpFiles" &>/dev/null
|
||||
appError "Failed to import tmp database: ${error:-$output}"
|
||||
return 1
|
||||
fi
|
||||
mariadbDatabaseRemove "$importDbName"
|
||||
mariadbUserRemove "$importDbUser"
|
||||
fi
|
||||
|
||||
function _siteAddRollback() {
|
||||
local message="$1"
|
||||
|
||||
[[ -n "$tmpFiles" ]] && rm -rf -- "$tmpFiles" &>/dev/null
|
||||
|
||||
appError "$message | Rollback..."
|
||||
siteRemove "$domain" || true
|
||||
}
|
||||
|
||||
if ! runError error openlitespeedVhostRebuild "$domain"; then
|
||||
_siteAddRollback "Failed vhost rebuild (1): $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error cp -a "$importFiles/." "$targetPath/www/"; then
|
||||
_siteAddRollback "Failed copying files: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error openlitespeedVhostRebuild "$domain"; then
|
||||
_siteAddRollback "Failed vhost rebuild (2): $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error openlitespeedConfigRebuild "$domain"; then
|
||||
_siteAddRollback "Failed OLS config rebuild: $error"
|
||||
return 1
|
||||
fi
|
||||
if ! runError error systemHostAdd "$domain"; then
|
||||
_siteAddRollback "Failed add domain to hosts: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local databasePass
|
||||
databasePass=$(siteConfigGetOrCreate "$domain" "databasePass")
|
||||
if ! runError error mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass"; then
|
||||
_siteAddRollback "Failed create user and database in MariaDB: $error"
|
||||
return 1
|
||||
fi
|
||||
if [[ -n "$sourceDatabase" ]]; then
|
||||
if ! run output error mariadbMasterImport "$databaseName" "$databaseUser" "$databasePass" "$sourceDatabase"; then
|
||||
_siteAddRollback "Failed to import data in MariaDB: ${error:-$output}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local redisPass
|
||||
redisPass=$(siteConfigGetOrCreate "$domain" "redisPass")
|
||||
if ! runError error redisUserSet "$redisUser" "$redisPass"; then
|
||||
_siteAddRollback "Failed create user in Redis: $error"
|
||||
return 1
|
||||
fi
|
||||
if ! runError error redisUserListSave; then
|
||||
_siteAddRollback "Failed update user list: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error postfixDomainAdd "$domain"; then
|
||||
_siteAddRollback "Failed add domain in Postfix: $error"
|
||||
return 1
|
||||
fi
|
||||
if ! runError error postfixPostmapRebuild; then
|
||||
_siteAddRollback "Failed postmap rebuild: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s' "$domain"
|
||||
}
|
||||
|
||||
# Removes a site: deletes its DB, Redis user, Postfix domain, OLS config, files, and system user.
|
||||
# $1 (domain): site domain name.
|
||||
function siteRemove() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
if [[ -f "$appDataPath/config/$domain.config" ]]; then
|
||||
local databaseName databaseUser redisUser output error
|
||||
|
||||
databaseName=$(siteConfigGet "$domain" "databaseName")
|
||||
if [[ -n "$databaseName" ]]; then
|
||||
runSilent mariadbDatabaseRemove "$databaseName"
|
||||
fi
|
||||
databaseUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
if [[ -n "$databaseUser" ]]; then
|
||||
runSilent mariadbUserRemove "$databaseUser"
|
||||
fi
|
||||
|
||||
redisUser=$(siteConfigGet "$domain" "redisUser")
|
||||
if [[ -n "$redisUser" ]]; then
|
||||
runSilent redisUserRemove "$redisUser"
|
||||
runSilent redisUserListSave
|
||||
fi
|
||||
fi
|
||||
|
||||
runSilent postfixDomainRemove "$domain"
|
||||
runSilent postfixPostmapRebuild
|
||||
runSilent systemHostRemove "$domain"
|
||||
runSilent openlitespeedConfigRebuild "$domain" delete
|
||||
runSilent openlitespeedConfigEdit vhost_up "$domain"
|
||||
|
||||
rm -f "$openlitespeedVhostsPath/$domain.conf" &>/dev/null
|
||||
rm -f "$openlitespeedVhostsPath/$domain.conf0" &>/dev/null
|
||||
rm -f "$openlitespeedVhostsPath/$domain.conf.txt" &>/dev/null
|
||||
rm -f "$appDataPath/config/$domain.config" &>/dev/null
|
||||
[[ -n "$domain" && "$domain" != "/" ]] && rm -rf "$vhostsPath/$domain" &>/dev/null
|
||||
|
||||
local ug
|
||||
ug=$(domainToUser "$domain")
|
||||
if [[ -n "$ug" && "$ug" != "root" ]]; then
|
||||
if id "$ug" >/dev/null 2>&1; then
|
||||
runSilent userdel "$ug"
|
||||
fi
|
||||
if getent group "$ug" >/dev/null 2>&1; then
|
||||
runSilent groupdel "$ug"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s' "$domain"
|
||||
}
|
||||
|
||||
# Copies a site: exports source DB, creates target site, rebuilds WP config.
|
||||
# $1 (domain): source site domain name.
|
||||
# $2 (domainNew): target site domain name.
|
||||
function siteCopy() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local domainNew
|
||||
domainNew=$(domainPrepare "$2")
|
||||
domainCheck "$domainNew" || return 1
|
||||
|
||||
if [[ "$domain" == "$domainNew" ]]; then
|
||||
appError "The domains match: $domain -> $domainNew"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local vhostList
|
||||
if ! run vhostList error openlitespeedVhostList all; then
|
||||
appError "Error retrieving vhost list: $error"
|
||||
return 1
|
||||
elif ! listContains "$domain" "$vhostList"; then
|
||||
appError "$domain: Not found in vhosts list"
|
||||
return 1
|
||||
elif listContains "$domainNew" "$vhostList"; then
|
||||
appError "$domainNew: Found in vhosts list"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local aliasList
|
||||
if ! run aliasList error openlitespeedAliasList; then
|
||||
appError "Error retrieving alias list: $error"
|
||||
return 1
|
||||
elif listContains "$domainNew" "$aliasList"; then
|
||||
appError "$domainNew: Found in alias list"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local databaseName databaseUser databasePass databaseFile
|
||||
databaseName=$(siteConfigGet "$domain" "databaseName")
|
||||
databaseUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
databasePass=$(siteConfigGet "$domain" "databasePass")
|
||||
databaseFile=$(mktemp) || return 1
|
||||
|
||||
trap 'rm -f -- "$databaseFile"' RETURN
|
||||
|
||||
if ! runError error mariadbMasterExport "$databaseUser" "$databasePass" "$databaseName" "$databaseFile"; then
|
||||
appError "$domain: Failed database export: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! runError error siteAdd "$domainNew" "$vhostsPath/$domain/www" "$databaseFile"; then
|
||||
appError "$domainNew: Create site: $error"
|
||||
return 1
|
||||
fi
|
||||
rm -f -- "$databaseFile"
|
||||
|
||||
if ! runError error wordpressDomainUpdate "$domainNew"; then
|
||||
appError "$domainNew: Failed config rebuild: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
trap - RETURN
|
||||
|
||||
printf '%s' "$domainNew"
|
||||
}
|
||||
|
||||
# Renames a site by copying it to the new domain and removing the old one.
|
||||
# $1 (domain): current site domain name.
|
||||
# $2 (domainNew): new site domain name.
|
||||
function siteRename() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local domainNew
|
||||
domainNew=$(domainPrepare "$2")
|
||||
domainCheck "$domainNew" || return 1
|
||||
|
||||
local error
|
||||
if ! runError error siteCopy "$domain" "$domainNew"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local aliasList
|
||||
if run aliasList error openlitespeedVhostAlias "$domain" && [[ -n "$aliasList" ]]; then
|
||||
local -a aliases
|
||||
mapfile -t aliases <<< "$aliasList"
|
||||
openlitespeedAliasSet "$domain"
|
||||
openlitespeedAliasSet "$domainNew" "${aliases[@]}"
|
||||
fi
|
||||
|
||||
runSilent siteRemove "$domain" || true
|
||||
|
||||
printf '%s' "$domainNew"
|
||||
}
|
||||
|
||||
# Resets all service passwords for a site and rebuilds MariaDB, Redis, Postfix, and WordPress config.
|
||||
# $1 (domain): site domain name.
|
||||
function sitePasswordReset() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local error vhostList
|
||||
run vhostList error openlitespeedVhostList || { appError "Failed get list of virtual hosts: $error"; return 1; }
|
||||
listContains "$domain" "$vhostList" || { appError "Domain is not exists in OpenLiteSpeed config"; return 1; }
|
||||
|
||||
local result=0
|
||||
|
||||
# reset passwords MariaDB
|
||||
local dbPass; dbPass=$(siteConfigGet "$domain" "databasePass")
|
||||
siteConfigSet "$domain" "databasePass"
|
||||
mariadbConfigRebuild "$domain" || { siteConfigSet "$domain" "databasePass" "$dbPass"; result=1; }
|
||||
|
||||
# reset passwords Redis
|
||||
local redisPass; redisPass=$(siteConfigGet "$domain" "redisPass")
|
||||
siteConfigSet "$domain" "redisPass"
|
||||
redisConfigRebuild "$domain" || { siteConfigSet "$domain" "redisPass" "$redisPass"; result=1; }
|
||||
|
||||
# reset passwords Postfix
|
||||
local postfixPass; postfixPass=$(siteConfigGet "$domain" "postfixPass")
|
||||
siteConfigSet "$domain" "postfixPass"
|
||||
postfixConfigRebuild "$domain" || { siteConfigSet "$domain" "postfixPass" "$postfixPass"; result=1; }
|
||||
|
||||
# update Wordpress config
|
||||
wordpressConfigRebuild "$domain" || result=1
|
||||
return $result
|
||||
}
|
||||
|
||||
# Resets all service credentials (passwords and usernames) and rebuilds all service configs for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function siteAuthReset() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local error vhostList
|
||||
run vhostList error openlitespeedVhostList || { appError "Failed get list of virtual hosts: $error"; return 1; }
|
||||
listContains "$domain" "$vhostList" || { appError "Domain is not exists in OpenLiteSpeed config"; return 1; }
|
||||
|
||||
local result=0
|
||||
|
||||
# reset passwords MariaDB
|
||||
local dbPass dbUser
|
||||
dbPass=$(siteConfigGet "$domain" "databasePass")
|
||||
dbUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
siteConfigSet "$domain" "databasePass"
|
||||
siteConfigSet "$domain" "databaseUser"
|
||||
mariadbConfigRebuild "$domain" || { siteConfigSet "$domain" "databasePass" "$dbPass"; siteConfigSet "$domain" "databaseUser" "$dbUser"; result=1; }
|
||||
|
||||
# reset passwords Redis
|
||||
local redisPass redisUser
|
||||
redisPass=$(siteConfigGet "$domain" "redisPass")
|
||||
redisUser=$(siteConfigGet "$domain" "redisUser")
|
||||
siteConfigSet "$domain" "redisPass"
|
||||
siteConfigSet "$domain" "redisUser"
|
||||
redisConfigRebuild "$domain" || { siteConfigSet "$domain" "redisPass" "$redisPass"; siteConfigSet "$domain" "redisUser" "$redisUser"; result=1; }
|
||||
|
||||
# reset passwords Postfix
|
||||
local postfixPass postfixUser
|
||||
postfixPass=$(siteConfigGet "$domain" "postfixPass")
|
||||
postfixUser=$(siteConfigGet "$domain" "postfixUser")
|
||||
siteConfigSet "$domain" "postfixPass"
|
||||
siteConfigSet "$domain" "postfixUser"
|
||||
postfixConfigRebuild "$domain" || { siteConfigSet "$domain" "postfixPass" "$postfixPass"; siteConfigSet "$domain" "postfixUser" "$postfixUser"; result=1; }
|
||||
|
||||
# update Wordpress config
|
||||
wordpressConfigRebuild "$domain" || result=1
|
||||
return $result
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
# Updates APT packages and installs required system dependencies.
|
||||
function systemApt() {
|
||||
apt update && apt upgrade -y || return 1
|
||||
apt install -y curl ipset iptables-persistent ipset-persistent jq zip mc nano idn2 acl xfsprogs opendkim-tools pigz
|
||||
}
|
||||
|
||||
# Creates ipset sets for WEDOS, WEDOS Global, and whitelist traffic, and installs an hourly update cron job.
|
||||
function systemIpset() {
|
||||
ipset list wedos &>/dev/null || ipset create wedos hash:ip family inet || {
|
||||
appError "Failed create ipset: wedos"
|
||||
return 1
|
||||
}
|
||||
ipset list wedos6 &>/dev/null || ipset create wedos6 hash:ip family inet6 || {
|
||||
appError "Failed create ipset: wedos6"
|
||||
return 1
|
||||
}
|
||||
ipset list wedos-global &>/dev/null || ipset create wedos-global hash:net family inet || {
|
||||
appError "Failed create ipset: wedos-global"
|
||||
return 1
|
||||
}
|
||||
ipset list wedos-global6 &>/dev/null || ipset create wedos-global6 hash:net family inet6 || {
|
||||
appError "Failed create ipset: wedos-global6"
|
||||
return 1
|
||||
}
|
||||
ipset list whitelist &>/dev/null || ipset create whitelist hash:ip family inet || {
|
||||
appError "Failed create ipset: whitelist"
|
||||
return 1
|
||||
}
|
||||
ipset list whitelist6 &>/dev/null || ipset create whitelist6 hash:ip family inet6 || {
|
||||
appError "Failed create ipset: whitelist6"
|
||||
return 1
|
||||
}
|
||||
|
||||
ipset add wedos 46.28.104.66 -exist
|
||||
ipset add wedos 46.28.107.200 -exist
|
||||
ipset add wedos 46.28.104.146 -exist
|
||||
ipset add wedos 46.28.107.215 -exist
|
||||
|
||||
local cron="/etc/cron.d/wedos-global-update"
|
||||
local job='0 * * * * root curl -fsSL https://ips.wedos.global/ips.json | jq -r '"'"'.list[]'"'"' | while read -r ip; do [[ "$ip" == *:* ]] && ipset add wedos-global6 "$ip" -exist || ipset add wedos-global "$ip" -exist; done >> /var/log/wedos-ipset-update.log 2>&1'
|
||||
printf '%s\n' "$job" > "$cron" || {
|
||||
appError "Failed write cron file: $cron"
|
||||
return 1
|
||||
}
|
||||
chmod 644 "$cron" || {
|
||||
appError "Failed chmod cron file: $cron"
|
||||
return 1
|
||||
}
|
||||
|
||||
(set -o pipefail; curl -fsSL https://ips.wedos.global/ips.json | jq -r '.list[]' | while read -r ip; do
|
||||
[[ "$ip" == *:* ]] && ipset add wedos-global6 "$ip" -exist || ipset add wedos-global "$ip" -exist
|
||||
done) >> /var/log/wedos-ipset-update.log 2>&1 || appError "Failed to update WEDOS Global IP sets."
|
||||
}
|
||||
|
||||
# Installs persistent iptables and ip6tables INPUT rules, then saves and reloads via netfilter-persistent.
|
||||
function systemIptables() {
|
||||
iptables -C INPUT -m set --match-set wedos src -j ACCEPT 2>/dev/null || iptables -I INPUT 1 -m set --match-set wedos src -j ACCEPT
|
||||
ip6tables -C INPUT -m set --match-set wedos6 src -j ACCEPT 2>/dev/null || ip6tables -I INPUT 1 -m set --match-set wedos6 src -j ACCEPT
|
||||
|
||||
iptables -C INPUT -m set --match-set wedos-global src -p tcp -m multiport --dports 80,443 -j ACCEPT 2>/dev/null || \
|
||||
iptables -I INPUT 2 -m set --match-set wedos-global src -p tcp -m multiport --dports 80,443 -j ACCEPT
|
||||
ip6tables -C INPUT -m set --match-set wedos-global6 src -p tcp -m multiport --dports 80,443 -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I INPUT 2 -m set --match-set wedos-global6 src -p tcp -m multiport --dports 80,443 -j ACCEPT
|
||||
|
||||
iptables -C INPUT -m set --match-set whitelist src -p tcp -m multiport --dports 80,443 -j ACCEPT 2>/dev/null || \
|
||||
iptables -I INPUT 3 -m set --match-set whitelist src -p tcp -m multiport --dports 80,443 -j ACCEPT
|
||||
ip6tables -C INPUT -m set --match-set whitelist6 src -p tcp -m multiport --dports 80,443 -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I INPUT 3 -m set --match-set whitelist6 src -p tcp -m multiport --dports 80,443 -j ACCEPT
|
||||
|
||||
iptables -C INPUT -i lo -j ACCEPT 2>/dev/null || iptables -I INPUT 4 -i lo -j ACCEPT
|
||||
ip6tables -C INPUT -i lo -j ACCEPT 2>/dev/null || ip6tables -I INPUT 4 -i lo -j ACCEPT
|
||||
|
||||
iptables -C INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || \
|
||||
iptables -I INPUT 5 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
ip6tables -C INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I INPUT 5 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
iptables -C INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT 2>/dev/null || \
|
||||
iptables -I INPUT 6 -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
|
||||
ip6tables -C INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT 2>/dev/null || \
|
||||
ip6tables -I INPUT 6 -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
|
||||
|
||||
iptables -C INPUT -j DROP 2>/dev/null || iptables -A INPUT -j DROP
|
||||
ip6tables -C INPUT -j DROP 2>/dev/null || ip6tables -A INPUT -j DROP
|
||||
|
||||
netfilter-persistent save || return 1
|
||||
netfilter-persistent reload || return 1
|
||||
}
|
||||
|
||||
# Returns non-empty root crontab lines; empty result if no crontab exists.
|
||||
function systemCronList() {
|
||||
local result
|
||||
if result="$(crontab -u root -l 2>&1)"; then
|
||||
:
|
||||
elif grep -qi 'no crontab for' <<< "$result"; then
|
||||
result=""
|
||||
else
|
||||
appError "Failed to retrieve cron jobs: $result"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$result" | awk 'NF'
|
||||
}
|
||||
|
||||
# Returns the IP address for a domain from /etc/hosts.
|
||||
# $1 (domain): domain name to look up.
|
||||
function systemHostGet() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
|
||||
awk -v domain="$domain" '
|
||||
$1 !~ /^#/ {
|
||||
for (i = 2; i <= NF; i++) {
|
||||
if ($i == domain) {
|
||||
print $1
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
' /etc/hosts
|
||||
}
|
||||
|
||||
# Adds a domain entry to /etc/hosts if not already present.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (ip): IP address (defaults to 127.0.0.1).
|
||||
function systemHostAdd() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ip="${2:-127.0.0.1}"
|
||||
|
||||
if ! awk -v ip="$ip" -v domain="$domain" '$1 == ip && $2 == domain { found=1 } END { exit !found }' /etc/hosts; then
|
||||
printf '%s %s\n' "$ip" "$domain" >> /etc/hosts || {
|
||||
appError "Failed writing hosts"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# Removes a domain entry from /etc/hosts.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (ip): IP address (defaults to 127.0.0.1).
|
||||
function systemHostRemove() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ip="${2:-127.0.0.1}"
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp) || return 1
|
||||
|
||||
if ! awk -v ip="$ip" -v domain="$domain" '!($1 == ip && $2 == domain)' /etc/hosts > "$tmp"; then
|
||||
rm -f "$tmp"
|
||||
appError "Failed editing hosts"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv -- "$tmp" /etc/hosts || {
|
||||
rm -f "$tmp"
|
||||
appError "Failed writing hosts"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Lists users in the SFTP access group.
|
||||
function sftpUserList() {
|
||||
getent group "$sftpAccessGroup" | awk -F: '{print $4}' | tr ',' '\n' | sed '/^$/d' | sort
|
||||
}
|
||||
|
||||
# Sets the SFTP password for a domain user from site config.
|
||||
function sftpPasswordSet() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
[[ -n "$domain" && "$domain" != "root" ]] || { appError "Incorrect or empty domain: $domain"; return 1; }
|
||||
|
||||
local ug sftpPass
|
||||
ug=$(domainToUser "$domain")
|
||||
|
||||
id "$ug" >/dev/null 2>&1 || { appError "User does not exist: $ug"; return 1; }
|
||||
sftpPass=$(siteConfigGetOrCreate "$domain" "sftpPass")
|
||||
[[ -n "$sftpPass" ]] || { appError "SFTP password is empty for domain: $domain"; return 1; }
|
||||
|
||||
printf '%s:%s\n' "$ug" "$sftpPass" | chpasswd || { appError "Change SFTP password failed for user: $ug"; return 1; }
|
||||
}
|
||||
|
||||
# Enables SFTP access for a domain user.
|
||||
function sftpAccessEnable() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
[[ -n "$domain" && "$domain" != "root" ]] || { appError "Incorrect or empty domain: $domain"; return 1; }
|
||||
|
||||
local output error ug
|
||||
ug=$(domainToUser "$domain")
|
||||
id "$ug" >/dev/null 2>&1 || { appError "User does not exist: $ug"; return 1; }
|
||||
[[ -d "$vhostsPath/$domain/www" ]] || { appError "Vhost www directory does not exist: $vhostsPath/$domain/www"; return 1; }
|
||||
|
||||
if ! id -nG "$ug" | tr ' ' '\n' | grep -Fxq "$sftpAccessGroup"; then
|
||||
run output error usermod -aG "$sftpAccessGroup" "$ug" || { appError "Add user $ug to $sftpAccessGroup: $error"; return 1; }
|
||||
fi
|
||||
|
||||
sftpPasswordSet "$domain"
|
||||
}
|
||||
|
||||
# Disables SFTP access for a domain user by removing them from the SFTP group.
|
||||
function sftpAccessDisable() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
[[ -n "$domain" && "$domain" != "root" ]] || { appError "Incorrect or empty domain: $domain"; return 1; }
|
||||
|
||||
local output error ug
|
||||
ug=$(domainToUser "$domain")
|
||||
id "$ug" >/dev/null 2>&1 || { appError "User does not exist: $ug"; return 1; }
|
||||
|
||||
if id -nG "$ug" | tr ' ' '\n' | grep -Fxq "$sftpAccessGroup"; then
|
||||
run output error gpasswd -d "$ug" "$sftpAccessGroup" || { appError "Remove user $ug from $sftpAccessGroup: $error"; return 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
# [WARNING] Patches sshd_config to enable SFTP via internal-sftp with group-based chroot.
|
||||
function sftpAddingSupport() {
|
||||
local sftpConfig="/etc/ssh/sshd_config"
|
||||
local sshService sshdBin sftpBackup output error
|
||||
|
||||
# printInfo "$systemLabel SFTP Adding support for SFTP access"
|
||||
[[ -f "$sftpConfig" ]] || { appError "SFTP Config not found: $sftpConfig"; return 1; }
|
||||
|
||||
if systemctl list-unit-files | grep -q '^sshd\.service'; then
|
||||
sshService="sshd"
|
||||
elif systemctl list-unit-files | grep -q '^ssh\.service'; then
|
||||
sshService="ssh"
|
||||
else
|
||||
appError "SFTP Service not found"
|
||||
return 1
|
||||
fi
|
||||
# printInfo "$systemLabel SFTP Use service: $sshService"
|
||||
|
||||
sshdBin="$(command -v sshd || true)"
|
||||
[[ -n "$sshdBin" ]] || { appError "SFTP Binary not found"; return 1; }
|
||||
|
||||
if ! getent group "$sftpAccessGroup" >/dev/null 2>&1; then
|
||||
# printInfo "$systemLabel SFTP Create SFTP access group: $sftpAccessGroup"
|
||||
run output error groupadd "$sftpAccessGroup" || { appError "SFTP Failed create group $sftpAccessGroup: $error"; return 1; }
|
||||
fi
|
||||
|
||||
grep -Eq '^[[:space:]]*Subsystem[[:space:]]+sftp[[:space:]]+' "$sftpConfig" || {
|
||||
appError "SFTP Not found Subsystem in $sftpConfig"
|
||||
return 1
|
||||
}
|
||||
|
||||
sftpBackup="$sftpConfig.$(date +%F_%H-%M-%S).bak"
|
||||
cp -a "$sftpConfig" "$sftpBackup" || { appError "SFTP Backup failed"; return 1; }
|
||||
|
||||
# printInfo "$systemLabel SFTP Update config..."
|
||||
if ! sed -i -E 's|^[[:space:]]*Subsystem[[:space:]]+sftp[[:space:]]+.*$|Subsystem sftp internal-sftp|' "$sftpConfig"; then
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
appError "SFTP Update Subsystem SFTP failed"
|
||||
return 1
|
||||
fi
|
||||
if ! sed -i \
|
||||
-e '/^# --- KUBE SFTP GLOBAL BEGIN ---$/,/^# --- KUBE SFTP GLOBAL END ---$/d' \
|
||||
-e '/^# --- KUBE SFTP GROUP BEGIN ---$/,/^# --- KUBE SFTP GROUP END ---$/d' \
|
||||
"$sftpConfig"; then
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
appError "SFTP Remove old SFTP blocks failed"
|
||||
return 1
|
||||
fi
|
||||
local tmpFile
|
||||
tmpFile="$(mktemp)"
|
||||
if ! {
|
||||
cat "$appAssetsPath/system/sftp-global.conf"
|
||||
echo
|
||||
cat "$sftpConfig"
|
||||
echo
|
||||
sed "s|{{sftp_access_group}}|$sftpAccessGroup|g" "$appAssetsPath/system/sftp-group.conf"
|
||||
} > "$tmpFile" || ! mv "$tmpFile" "$sftpConfig"; then
|
||||
rm -f "$tmpFile"
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
appError "SFTP Append SFTP config blocks failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# printInfo "$systemLabel SFTP Config validation..."
|
||||
if ! run output error "$sshdBin" -t -f "$sftpConfig"; then
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
appError "SFTP Config validation failed: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# printInfo "$systemLabel SFTP Reload/restart service..."
|
||||
if ! run output error systemctl reload "$sshService"; then
|
||||
if ! run output error systemctl restart "$sshService"; then
|
||||
cp -a "$sftpBackup" "$sftpConfig"
|
||||
systemctl restart "$sshService" >/dev/null 2>&1 || true
|
||||
appError "SFTP Reload/restart failed: $error"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# printSuccess "$systemLabel SFTP Adding support complete"
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
# Lists WordPress users via WP-CLI.
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressUserList() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
wordpressExec "$domain" user list
|
||||
}
|
||||
|
||||
# Sets a WordPress user's password via WP-CLI.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (user): WordPress username or ID.
|
||||
# $3 (pass): new password.
|
||||
function wordpressPasswordSet() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local user="$2"
|
||||
[[ -n "$user" ]] || { appError "User not specified"; return 1; }
|
||||
|
||||
local pass="$3"
|
||||
[[ -n "$pass" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
wordpressExec "$domain" user update "$user" --user_pass="$pass"
|
||||
}
|
||||
|
||||
# Runs WP-CLI inside the OLS pod for the specified domain.
|
||||
# $1 (domain): site domain name.
|
||||
# $2+ (...): WP-CLI arguments.
|
||||
function wordpressExec() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
domain=$(domainPrepare "$domain")
|
||||
shift
|
||||
|
||||
openlitespeedExec wp --path=/var/www/vhosts/"$domain"/www --allow-root --skip-plugins --skip-themes --require=/var/www/data/"$domain"/bootstrap.php --exec='error_reporting(0);define("WP_DEBUG",false);' "$@"
|
||||
}
|
||||
|
||||
function wordpressSalt() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
|
||||
domain=$(domainPrepare "$domain")
|
||||
shift
|
||||
|
||||
wordpressExec "$domain" config shuffle-salts;
|
||||
}
|
||||
|
||||
# Replaces a string in the WordPress database via WP-CLI search-replace.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (search): string to search for.
|
||||
# $3 (replace): replacement string.
|
||||
function wordpressSearchReplace() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local search="$2"
|
||||
[[ -n "$search" ]] || { appError "Search string not specified"; return 1; }
|
||||
|
||||
local replace="$3"
|
||||
[[ -n "$replace" ]] || { appError "Replacement string not specified"; return 1; }
|
||||
|
||||
[[ "$search" == "$replace" ]] && return 0
|
||||
|
||||
wordpressExec "$domain" search-replace "$search" "$replace" --all-tables-with-prefix --precise --skip-columns=guid --report-changed-only
|
||||
}
|
||||
|
||||
# Writes the bootstrap.php configuration file for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressConfigUpdate() {
|
||||
local domain="$1"
|
||||
domain=$(domainPrepare "$domain")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local ug bootstrapFile tmpFile
|
||||
ug=$(domainToUser "$domain")
|
||||
bootstrapFile="$openlitespeedVhostDataPath/$domain/bootstrap.php"
|
||||
tmpFile="$bootstrapFile".tmp
|
||||
|
||||
local databaseName databaseUser databasePass redisUser redisPass postfixUser postfixPass key
|
||||
databaseName="$(siteConfigGet "$domain" "databaseName")"
|
||||
databaseUser="$(siteConfigGet "$domain" "databaseUser")"
|
||||
databasePass="$(siteConfigGet "$domain" "databasePass")"
|
||||
redisUser="$(siteConfigGet "$domain" "redisUser")"
|
||||
redisPass="$(siteConfigGet "$domain" "redisPass")"
|
||||
postfixUser="$(siteConfigGet "$domain" "postfixUser")"
|
||||
postfixPass="$(siteConfigGet "$domain" "postfixPass")"
|
||||
for key in databaseName databaseUser databasePass redisUser redisPass postfixUser postfixPass; do
|
||||
[[ -n "${!key}" ]] || { appError "wordpressConfigUpdate: $key is empty"; return 1; }
|
||||
done
|
||||
|
||||
cat > "$tmpFile" <<PHP
|
||||
<?php
|
||||
|
||||
define('DB_HOST', '$mariadbMasterKube');
|
||||
define('DB_NAME', '$databaseName');
|
||||
define('DB_USER', '$databaseUser');
|
||||
define('DB_PASSWORD', '$databasePass');
|
||||
|
||||
define('WP_REDIS_SELECTIVE_FLUSH', true);
|
||||
define('WP_REDIS_PASSWORD', ['$redisUser', '$redisPass']);
|
||||
define('WP_REDIS_PREFIX', '${redisUser}:');
|
||||
define('WP_REDIS_PORT', 6379);
|
||||
define('WP_REDIS_HOST', '${redisKube}-master');
|
||||
define('WP_REDIS_CLIENT', 'phpredis');
|
||||
|
||||
define('SODEW_SMTP_USER', '${postfixUser}@${postfixDefaultRealm}');
|
||||
define('SODEW_SMTP_PASS', '$postfixPass');
|
||||
define('SODEW_SMTP_HOST', '$postfixHost');
|
||||
define('SODEW_SMTP_POSTMASTER', '$postfixPostmaster');
|
||||
PHP
|
||||
|
||||
chown -R -- "$ug:$ug" "$tmpFile" || { appError "Change owner of vhost data directory failed: $tmpFile"; return 1; }
|
||||
chmod 600 -- "$tmpFile" || { appError "Change permissions of vhost data files failed: $tmpFile"; return 1; }
|
||||
mv -f -- "$tmpFile" "$bootstrapFile" || return 1
|
||||
}
|
||||
|
||||
# Wraps WordPress define() calls to be conditional (defined() || define(...)).
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressConfigDefine() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local configFile="$vhostsPath/$domain/www/wp-config.php"
|
||||
[[ -f "$configFile" ]] || { appError "File not found: $configFile"; return 1; }
|
||||
|
||||
local constants=(
|
||||
DB_NAME
|
||||
DB_USER
|
||||
DB_PASSWORD
|
||||
DB_HOST
|
||||
DB_CHARSET
|
||||
DB_COLLATE
|
||||
WP_REDIS_SELECTIVE_FLUSH
|
||||
WP_REDIS_PASSWORD
|
||||
WP_REDIS_PREFIX
|
||||
WP_REDIS_PORT
|
||||
WP_REDIS_HOST
|
||||
WP_REDIS_CLIENT
|
||||
SODEW_SMTP_USER
|
||||
SODEW_SMTP_PASS
|
||||
SODEW_SMTP_HOST
|
||||
SODEW_SMTP_POSTMASTER
|
||||
)
|
||||
|
||||
local const
|
||||
for const in "${constants[@]}"; do
|
||||
sed -i -E "s@^([[:space:]]*)(define[[:space:]]*\([[:space:]]*['\"]${const}['\"][[:space:]]*,)@\1defined('${const}') || \2@" "$configFile"
|
||||
done
|
||||
}
|
||||
|
||||
# Rebuilds WordPress configuration for a site: defines, bootstrap, and mu-plugins.
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressConfigRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
wordpressConfigDefine "$domain" || return 1
|
||||
wordpressConfigUpdate "$domain" || return 1
|
||||
|
||||
local muPluginsPath ug
|
||||
muPluginsPath="$vhostsPath/$domain/www/wp-content/mu-plugins"
|
||||
ug=$(domainToUser "$domain")
|
||||
|
||||
[[ -d "$muPluginsPath" ]] || mkdir -p -- "$muPluginsPath" || { appError "Create directory failed: $muPluginsPath"; return 1; }
|
||||
cp -f -- "$appAssetsPath/wordpress/shared/00-hosting-loader.php" "$muPluginsPath/00-hosting-loader.php" || { appError "Copy file failed: $muPluginsPath/00-hosting-loader.php"; return 1; }
|
||||
chown -R -- "$ug:$ug" "$muPluginsPath" || { appError "Change owner of directory failed: $muPluginsPath"; return 1; }
|
||||
chmod 2750 "$muPluginsPath" || { appError "Change permissions of directories failed: $muPluginsPath"; return 1; }
|
||||
chmod 0640 "$muPluginsPath/00-hosting-loader.php" || { appError "Change permissions of file failed: $muPluginsPath/00-hosting-loader.php"; return 1; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
# Placeholder for extended WordPress config rebuild (not yet implemented).
|
||||
# $1 (domain): site domain name.
|
||||
function wordpressConfigRebuildEx() {
|
||||
echo '*****'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Updates all WordPress URLs in DB to match the current domain, cleans cache and Redis.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (abspathOld): old absolute path to replace.
|
||||
function wordpressDomainUpdate() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { printDanger "$wordpressLabel Domain not specified"; return 1; }
|
||||
local siteNew="https://$domain"
|
||||
|
||||
local abspathOld="$2"
|
||||
|
||||
local isMultiSite
|
||||
isMultiSite=$(wordpressExec "$domain" eval 'echo is_multisite() ? 1 : 0;')
|
||||
if [[ "$isMultiSite" == "1" ]]; then
|
||||
printDanger "$wordpressLabel This script is for SINGLE SITE only. Detected multisite. Abort."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local siteConst homeConst siteOption homeOption
|
||||
siteConst=$(wordpressExec "$domain" eval 'echo defined("WP_SITEURL")?WP_SITEURL:"";' || true)
|
||||
siteConst=$(strTrimSlash "$siteConst")
|
||||
homeConst=$(wordpressExec "$domain" eval 'echo defined("WP_HOME")?WP_HOME:"";' || true)
|
||||
homeConst=$(strTrimSlash "$homeConst")
|
||||
siteOption=$(wordpressExec "$domain" option get siteurl 2>/dev/null || true)
|
||||
siteOption=$(strTrimSlash "$siteOption")
|
||||
homeOption=$(wordpressExec "$domain" option get home 2>/dev/null || true)
|
||||
homeOption=$(strTrimSlash "$homeOption")
|
||||
printInfo "$wordpressLabel Wordpress siteurl | Const: $siteConst | Option: $siteOption"
|
||||
printInfo "$wordpressLabel Wordpress home | Const: $homeConst | Option: $homeOption"
|
||||
|
||||
local siteOld="${siteConst:-$siteOption}"
|
||||
if [[ -z "$siteOld" ]]; then
|
||||
siteOld="${homeConst:-$homeOption}"
|
||||
fi
|
||||
if [[ -z "$siteOld" ]]; then
|
||||
appError "Could not detect siteOld (neither constants nor DB options present)."
|
||||
return 2
|
||||
fi
|
||||
local schemeOld hostOld rootOld
|
||||
schemeOld=$(printf '%s' "$siteOld" | awk -F:// '{print $1}')
|
||||
hostOld=$(printf '%s' "$siteOld" | awk -F[/:] '{print $4}')
|
||||
rootOld="$schemeOld://$hostOld"
|
||||
printInfo "$wordpressLabel Wordpress OLD | Site: $siteOld | Scheme: $schemeOld | Host: $hostOld | Root: $rootOld"
|
||||
|
||||
local sitePath homePath
|
||||
sitePath=$(wordpressExec "$domain" eval 'echo parse_url(get_option("siteurl"), PHP_URL_PATH)?:"";' || true)
|
||||
[[ "$sitePath" == "/" ]] && sitePath=""
|
||||
homePath=$(wordpressExec "$domain" eval 'echo parse_url(get_option("home"), PHP_URL_PATH)?:"";' || true)
|
||||
[[ "$homePath" == "/" ]] && homePath=""
|
||||
|
||||
local wpType="unknown"
|
||||
if [[ -z "$homePath" && -z "$sitePath" ]]; then wpType="single_root"
|
||||
elif [[ -n "$homePath" && -n "$sitePath" && "$homePath" == "$sitePath" ]]; then wpType="single_subdir"
|
||||
elif [[ -z "$homePath" && -n "$sitePath" ]]; then wpType="single_mixed"
|
||||
fi
|
||||
printInfo "$wordpressLabel Wordpress OLD | Type: $wpType"
|
||||
|
||||
local siteConstHas homeConstHas
|
||||
siteConstHas=$(wordpressExec "$domain" eval 'echo defined("WP_SITEURL")?1:0;')
|
||||
if [[ "$siteConstHas" == "1" ]]; then
|
||||
wordpressExec "$domain" config delete WP_SITEURL --type=constant || true
|
||||
fi
|
||||
homeConstHas=$(wordpressExec "$domain" eval 'echo defined("WP_HOME")?1:0;')
|
||||
if [[ "$homeConstHas" == "1" ]]; then
|
||||
wordpressExec "$domain" config delete WP_HOME --type=constant || true
|
||||
fi
|
||||
|
||||
printInfo "$wordpressLabel Update siteurl: $siteNew"
|
||||
wordpressExec "$domain" option update siteurl "$siteNew"
|
||||
|
||||
printInfo "$wordpressLabel Update home: $siteNew"
|
||||
wordpressExec "$domain" option update home "$siteNew"
|
||||
|
||||
printInfo "$wordpressLabel Replace in DB (1): $siteOld --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$siteOld" "$siteNew"
|
||||
if [[ -n "$homeOption" && "$homeOption" != "$siteOld" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (2): $homeOption --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$homeOption" "$siteNew"
|
||||
fi
|
||||
if [[ -n "$siteOption" && "$siteOption" != "$siteOld" && "$siteOption" != "$homeOption" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (3): $siteOption --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$siteOption" "$siteNew"
|
||||
fi
|
||||
if [[ -n "$hostOld" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (4): //$hostOld --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "//$hostOld" "$siteNew"
|
||||
fi
|
||||
if [[ "$wpType" == "single_mixed" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (5): $rootOld --> $siteNew"
|
||||
wordpressSearchReplace "$domain" "$rootOld" "$siteNew"
|
||||
fi
|
||||
if [[ -n "$abspathOld" ]]; then
|
||||
printInfo "$wordpressLabel Replace in DB (6): $abspathOld --> /var/www/vhosts/$domain/www"
|
||||
wordpressSearchReplace "$domain" "$abspathOld" "/var/www/vhosts/$domain/www"
|
||||
fi
|
||||
|
||||
printInfo "$wordpressLabel Replace in DB (7): $hostOld --> $domain"
|
||||
wordpressSearchReplace "$domain" "$hostOld" "$domain"
|
||||
|
||||
printInfo "$wordpressLabel Delete options: upload_path/upload_url_path..."
|
||||
wordpressExec "$domain" option delete upload_path || true
|
||||
wordpressExec "$domain" option delete upload_url_path || true
|
||||
|
||||
printInfo "$wordpressLabel Clean cache..."
|
||||
wordpressExec "$domain" transient delete --all || true
|
||||
|
||||
printInfo "$wordpressLabel Redis clean..."
|
||||
redisDomainClean "$domain" || true
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Restart worker deployment and wait until ready.
|
||||
function workerRestart() {
|
||||
k3sRun rollout restart deployment/"$workerKube" || return 1
|
||||
k3sRun rollout status deployment/"$workerKube" --timeout=180s
|
||||
}
|
||||
|
||||
# Pause the worker agent via control file.
|
||||
function workerAgentStop() {
|
||||
printf '%s\n%s\n%s\n' "action=pause" "status=$taskStatusExecution" "start=$(date +%s)" > "$workerTasksPath/pause.task" || {
|
||||
appError "Failed to write pause task: $workerTasksPath/pause.task"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Remove pause flag for the worker agent.
|
||||
function workerAgentStart() {
|
||||
rm -f "$workerTasksPath/pause.task"
|
||||
}
|
||||
Reference in New Issue
Block a user