soubory SODEW

This commit is contained in:
2026-08-12 10:14:13 +02:00
parent 9fc81b53cc
commit f186ec26a8
116 changed files with 20087 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
.zed/
_tmp/
data/
config.sh
_gen.sh
CLAUDE.md
codebook.toml
+123
View File
@@ -0,0 +1,123 @@
# KUBE 7.1.553
Bash script for deploying/backups and restore SODEW infrastructures.
> [!WARNING]
> Documentation is outdated
### Install HELM
```
curl -fsSL https://get.helm.sh/helm-v3.16.2-linux-amd64.tar.gz | tar -xz
sudo mv linux-amd64/helm /usr/local/bin/helm
helm version
```
---
- [File structure](docs/file_scructure.md)
- [Options](docs/options.md)
- Usage
- [Backup](docs/backup.md)
- [Restore](docs/restore.md)
- [Storage](docs/storage.md)
- [k3s](docs/k3s.md)
- [MariaDB](docs/resources/mariadb.md)
- [Redis](docs/resources/redis.md)
- [OpenLiteSpeed](docs/resources/openlitespeed.md)
- [Postfix](docs/resources/postfix.md)
- [Worker](docs/resources/worker.md)
- [Site/Wordpress](docs/site.md)
- [Transfer](docs/transfer.md)
- [Shell](docs/shell.md)
- [Log](docs/log.md)
- [Cron](docs/cron.md)
- [Test](docs/test.md)
- [Install](docs/install.md)
- [Script](docs/script.md)
- [Mail server](docs/mta.md)
## Getting started
Before you start, you must create a configuration file:
```bash
cp config.sh.default config.sh
```
and make the necessary changes.
If necessary, you can set your own password values for MariaDB, Redis, rSync, OpenLiteSpeed ...
This can be done by specifying values for the corresponding variables or by writing values to the corresponding files in the `data` directory. If there are no files, just run the script without parameters. The files with passwords will be created automatically. After that you can make the appropriate edits.
## Usage
The script requires elevated permissions to work (sudoers group).
## Scheme open ports
```mermaid
flowchart LR;
subgraph MD[MariaDB replica]
MDM[mariadb-master-0]
MDS[mariadb-slave-0]
end
subgraph RD[Redis cluster]
RD0[redis-0]
RD1[redis-1]
RD2[redis-2]
RS0[redis-sentinel-0]
RS1[redis-sentinel-1]
RS2[redis-sentinel-2]
end
subgraph OLS[OpenLiteSpeed]
OL1[openlitespeed-0]
OL2[openlitespeed-1]
end
PTF[postfix-xxxxxxxxx-xxxxx]
MDM <==> MDS
RD0 <==> RD1 <==> RD2 <==> RD0
RS0 <==> RS1 <==> RS2 <==> RS0
P3306([3306])
P3306 --mysql--> MD
P6379([6379])
P26379([26379])
P6379 & P26379 --redis--> RD
P80([80/443])
P80 --http/https--> OLS
P7080([7080/31080])
P7080 --admin panel--> OLS
P25([25])
P587([587])
P25 --smtp--> PTF
P587 --smtp=tls--> PTF
```
## Scheme use ports
```mermaid
%%graph LR;
flowchart LR;
subgraph K3S[Server]
MD[MariaDB]
RD[Redis]
PTF[Postfix]
subgraph OLS[OpenLiteSpeed]
WP[Sites on Wordpress]
ADM[Admin Panel]
end
end
NET[Internet]
NET --80,443--> WP
NET --31080--> ADM
NET --25--> PTF
WP --3306--> MD
WP --6379--> RD
WP --587--> PTF
``````
+51
View File
@@ -0,0 +1,51 @@
#mta.krumax.cz
#api.krumax.cz
#us1.krumax.cz
us2.krumax.cz
us3.krumax.cz
us4.krumax.cz
us5.krumax.cz
us6.krumax.cz
us7.krumax.cz
us8.krumax.cz
us9.krumax.cz
us10.krumax.cz
us11.krumax.cz
us12.krumax.cz
us13.krumax.cz
us14.krumax.cz
us15.krumax.cz
us16.krumax.cz
us17.krumax.cz
us18.krumax.cz
us19.krumax.cz
us20.krumax.cz
us21.krumax.cz
us22.krumax.cz
us23.krumax.cz
us24.krumax.cz
us25.krumax.cz
us26.krumax.cz
us27.krumax.cz
us28.krumax.cz
us29.krumax.cz
us30.krumax.cz
us31.krumax.cz
us32.krumax.cz
us33.krumax.cz
us34.krumax.cz
us35.krumax.cz
us36.krumax.cz
us37.krumax.cz
us38.krumax.cz
us39.krumax.cz
us40.krumax.cz
us41.krumax.cz
us42.krumax.cz
us43.krumax.cz
us44.krumax.cz
us45.krumax.cz
us46.krumax.cz
us47.krumax.cz
us48.krumax.cz
us49.krumax.cz
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v2
name: stack
version: 0.1.0
type: application
@@ -0,0 +1,16 @@
profiles:
mariadbMaster:
cpuRequest: 1000m
cpuLimit: 4000m
threadPoolSize: 4
mariadbSlave:
cpuRequest: 250m
cpuLimit: 1000m
threadPoolSize: 1
redis:
cpuRequest: 250m
cpuLimit: 1000m
maxClients: 20000
openlitespeed:
cpuRequest: 3000m
cpuLimit: 7000m
@@ -0,0 +1,16 @@
profiles:
mariadbMaster:
cpuRequest: 2000m
cpuLimit: 8000m
threadPoolSize: 6
mariadbSlave:
cpuRequest: 500m
cpuLimit: 2000m
threadPoolSize: 1
redis:
cpuRequest: 750m
cpuLimit: 4000m
maxClients: 30000
openlitespeed:
cpuRequest: 5000m
cpuLimit: 12000m
@@ -0,0 +1,16 @@
profiles:
mariadbMaster:
cpuRequest: 500m
cpuLimit: 1500m
threadPoolSize: 2
mariadbSlave:
cpuRequest: 100m
cpuLimit: 500m
threadPoolSize: 1
redis:
cpuRequest: 100m
cpuLimit: 500m
maxClients: 8000
openlitespeed:
cpuRequest: 750m
cpuLimit: 2000m
@@ -0,0 +1,16 @@
profiles:
mariadbMaster:
cpuRequest: 750m
cpuLimit: 2500m
threadPoolSize: 3
mariadbSlave:
cpuRequest: 200m
cpuLimit: 750m
threadPoolSize: 1
redis:
cpuRequest: 150m
cpuLimit: 750m
maxClients: 12000
openlitespeed:
cpuRequest: 1250m
cpuLimit: 3000m
@@ -0,0 +1,26 @@
profiles:
mariadbMaster:
memoryRequest: 28Gi
memoryLimit: 40Gi
maxConnections: 600
innodbBufferPoolSize: 30G
innodbBufferPoolInstances: 16
tableOpenCache: 16000
tableOpenCacheInstances: 16
tmpTableSize: 64M
mariadbSlave:
memoryRequest: 8Gi
memoryLimit: 12Gi
maxConnections: 50
innodbBufferPoolSize: 8G
innodbBufferPoolInstances: 8
tableOpenCache: 8000
tableOpenCacheInstances: 8
tmpTableSize: 64M
redis:
memoryRequest: 2Gi
memoryLimit: 5Gi
maxmemory: 3500mb
openlitespeed:
memoryRequest: 8Gi
memoryLimit: 24Gi
@@ -0,0 +1,26 @@
profiles:
mariadbMaster:
memoryRequest: 2Gi
memoryLimit: 4Gi
maxConnections: 80
innodbBufferPoolSize: 2G
innodbBufferPoolInstances: 2
tableOpenCache: 2000
tableOpenCacheInstances: 4
tmpTableSize: 8M
mariadbSlave:
memoryRequest: 512Mi
memoryLimit: 1Gi
maxConnections: 30
innodbBufferPoolSize: 512M
innodbBufferPoolInstances: 1
tableOpenCache: 1000
tableOpenCacheInstances: 2
tmpTableSize: 8M
redis:
memoryRequest: 128Mi
memoryLimit: 512Mi
maxmemory: 350mb
openlitespeed:
memoryRequest: 2Gi
memoryLimit: 4Gi
@@ -0,0 +1,26 @@
profiles:
mariadbMaster:
memoryRequest: 4Gi
memoryLimit: 8Gi
maxConnections: 150
innodbBufferPoolSize: 5G
innodbBufferPoolInstances: 5
tableOpenCache: 4000
tableOpenCacheInstances: 8
tmpTableSize: 16M
mariadbSlave:
memoryRequest: 1Gi
memoryLimit: 2Gi
maxConnections: 50
innodbBufferPoolSize: 1G
innodbBufferPoolInstances: 1
tableOpenCache: 2000
tableOpenCacheInstances: 4
tmpTableSize: 16M
redis:
memoryRequest: 256Mi
memoryLimit: 1Gi
maxmemory: 700mb
openlitespeed:
memoryRequest: 3Gi
memoryLimit: 6Gi
@@ -0,0 +1,26 @@
profiles:
mariadbMaster:
memoryRequest: 8Gi
memoryLimit: 16Gi
maxConnections: 250
innodbBufferPoolSize: 10G
innodbBufferPoolInstances: 10
tableOpenCache: 8000
tableOpenCacheInstances: 16
tmpTableSize: 32M
mariadbSlave:
memoryRequest: 2Gi
memoryLimit: 4Gi
maxConnections: 50
innodbBufferPoolSize: 2G
innodbBufferPoolInstances: 2
tableOpenCache: 4000
tableOpenCacheInstances: 8
tmpTableSize: 32M
redis:
memoryRequest: 512Mi
memoryLimit: 2Gi
maxmemory: 1500mb
openlitespeed:
memoryRequest: 8Gi
memoryLimit: 16Gi
@@ -0,0 +1,19 @@
{{- if .Values.debugHelm }}
---
apiVersion: v1
kind: Pod
metadata: { name: debug, namespace: "{{ .Values.namespace }}" }
spec:
containers:
- name: debug
image: nicolaka/netshoot:latest
command: ["sh","-c","sleep infinity"]
stdin: true
tty: true
securityContext:
capabilities:
add: ["NET_RAW","NET_ADMIN"] # for tcpdump/mtr
restartPolicy: Never
{{- end }}
@@ -0,0 +1,299 @@
{{- if .Values.mariadbHelm }}
---
apiVersion: v1
kind: ConfigMap
metadata: { name: "{{ .Values.mariadbMaster }}-config", namespace: "{{ .Values.namespace }}" }
data:
replication.cnf: |
[mysqld]
skip_name_resolve=1
server-id=1
# --- log ---
log_bin=mysql-bin
binlog_format=ROW
binlog_expire_logs_seconds=604800
max_binlog_total_size=32212254720
# --- durability ---
innodb_flush_log_at_trx_commit=1
sync_binlog=1
# --- connection / thread pool ---
max_connections={{ .Values.profiles.mariadbMaster.maxConnections }}
thread_handling=pool-of-threads
thread_pool_size={{ .Values.profiles.mariadbMaster.threadPoolSize }}
thread_pool_max_threads=128
thread_pool_stall_limit=500
innodb_buffer_pool_size={{ .Values.profiles.mariadbMaster.innodbBufferPoolSize }}
innodb_buffer_pool_instances={{ .Values.profiles.mariadbMaster.innodbBufferPoolInstances }}
innodb_flush_method=O_DIRECT
innodb_flush_neighbors=0
innodb_io_capacity=2000
innodb_io_capacity_max=4000
innodb_log_file_size=1G
innodb_log_buffer_size=64M
# --- cache ---
query_cache_type=0
query_cache_size=0
table_open_cache={{ .Values.profiles.mariadbMaster.tableOpenCache }}
table_open_cache_instances={{ .Values.profiles.mariadbMaster.tableOpenCacheInstances }}
table_definition_cache={{ .Values.profiles.mariadbMaster.tableOpenCache }}
open_files_limit=65535
# --- buffers ---
tmp_table_size={{ .Values.profiles.mariadbMaster.tmpTableSize }}
max_heap_table_size={{ .Values.profiles.mariadbMaster.tmpTableSize }}
sort_buffer_size=2M
join_buffer_size=2M
read_buffer_size=256K
read_rnd_buffer_size=512K
# --- timeouts ---
wait_timeout=60
interactive_timeout=300
max_allowed_packet=64M
slow_query_log=1
long_query_time=1
slow_query_log_file=/var/lib/mysql/slow.log
log_error=/var/lib/mysql/error.log
---
apiVersion: v1
kind: ConfigMap
metadata: { name: "{{ .Values.mariadbSlave }}-config", namespace: "{{ .Values.namespace }}" }
data:
replication.cnf: |
[mysqld]
skip_name_resolve=1
server-id=2
read_only=1
# --- log ---
relay-log=relay-log
relay_log_purge=1
relay_log_recovery=1
# --- connection / thread pool ---
max_connections={{ .Values.profiles.mariadbSlave.maxConnections }}
thread_handling=pool-of-threads
thread_pool_size={{ .Values.profiles.mariadbSlave.threadPoolSize }}
thread_pool_max_threads=32
thread_pool_stall_limit=500
# --- InnoDB ---
innodb_buffer_pool_size={{ .Values.profiles.mariadbSlave.innodbBufferPoolSize }}
innodb_buffer_pool_instances={{ .Values.profiles.mariadbSlave.innodbBufferPoolInstances }}
innodb_flush_method=O_DIRECT
innodb_flush_neighbors=0
innodb_io_capacity=1000
innodb_io_capacity_max=2000
innodb_log_file_size=512M
innodb_log_buffer_size=32M
# --- durability (for standby recovery replica) ---
innodb_flush_log_at_trx_commit=1
sync_binlog=1
# --- cache ---
query_cache_type=0
query_cache_size=0
table_open_cache={{ .Values.profiles.mariadbSlave.tableOpenCache }}
table_open_cache_instances={{ .Values.profiles.mariadbSlave.tableOpenCacheInstances }}
table_definition_cache={{ .Values.profiles.mariadbSlave.tableOpenCache }}
open_files_limit=65535
# --- buffers ---
tmp_table_size={{ .Values.profiles.mariadbSlave.tmpTableSize }}
max_heap_table_size={{ .Values.profiles.mariadbSlave.tmpTableSize }}
sort_buffer_size=1M
join_buffer_size=1M
read_buffer_size=256K
read_rnd_buffer_size=512K
# --- timeouts ---
wait_timeout=60
interactive_timeout=300
max_allowed_packet=64M
# --- logs ---
slow_query_log=0
log_error=/var/lib/mysql/error.log
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.mariadbMaster }}-pv" }
spec:
capacity: { storage: 100Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteOnce ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.mariadbMasterPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.mariadbMaster }}-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.mariadbMaster }}-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteOnce ]
resources: { requests: { storage: 100Gi } }
storageClassName: ""
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.mariadbSlave }}-pv" }
spec:
capacity: { storage: 100Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteOnce ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.mariadbSlavePath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.mariadbSlave }}-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.mariadbSlave }}-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteOnce ]
resources: { requests: { storage: 100Gi } }
storageClassName: ""
---
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: "{{ .Values.mariadbMaster }}", namespace: "{{ .Values.namespace }}" }
spec:
serviceName: "{{ .Values.mariadbMaster }}-headless"
replicas: 1
selector: { matchLabels: { app: "{{ .Values.mariadbMaster }}" } }
template:
metadata: { labels: { app: "{{ .Values.mariadbMaster }}" } }
spec:
terminationGracePeriodSeconds: 60
containers:
- name: "{{ .Values.mariadbMaster }}"
image: mariadb:12.2
resources:
requests: { cpu: "{{ .Values.profiles.mariadbMaster.cpuRequest }}", memory: "{{ .Values.profiles.mariadbMaster.memoryRequest }}" }
limits: { cpu: "{{ .Values.profiles.mariadbMaster.cpuLimit }}", memory: "{{ .Values.profiles.mariadbMaster.memoryLimit }}" }
ports:
- { containerPort: 3306 }
env:
- { name: MARIADB_ROOT_PASSWORD, valueFrom: { secretKeyRef: { name: "{{ .Values.mariadb }}-secret", key: root-password } } }
readinessProbe:
exec:
command: ["sh","-lc",'mariadb-admin ping -h 127.0.0.1 -uroot -p"$MARIADB_ROOT_PASSWORD" --silent']
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
exec:
command: ["sh","-lc",'mariadb-admin ping -h 127.0.0.1 -uroot -p"$MARIADB_ROOT_PASSWORD" --silent']
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
volumeMounts:
- { name: "{{ .Values.mariadbMaster }}-volume", mountPath: /var/lib/mysql }
- { name: "{{ .Values.mariadbMaster }}-config-volume", mountPath: /etc/mysql/conf.d/replication.cnf, subPath: replication.cnf }
volumes:
- name: "{{ .Values.mariadbMaster }}-volume"
persistentVolumeClaim: { claimName: "{{ .Values.mariadbMaster }}-pvc" }
- name: "{{ .Values.mariadbMaster }}-config-volume"
configMap: { name: "{{ .Values.mariadbMaster }}-config" }
---
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: "{{ .Values.mariadbSlave }}", namespace: "{{ .Values.namespace }}" }
spec:
serviceName: "{{ .Values.mariadbSlave }}-headless"
replicas: 1
selector: { matchLabels: { app: "{{ .Values.mariadbSlave }}" } }
template:
metadata: { labels: { app: "{{ .Values.mariadbSlave }}" } }
spec:
terminationGracePeriodSeconds: 60
containers:
- name: "{{ .Values.mariadbSlave }}"
image: mariadb:12.2
resources:
requests: { cpu: "{{ .Values.profiles.mariadbSlave.cpuRequest }}", memory: "{{ .Values.profiles.mariadbSlave.memoryRequest }}" }
limits: { cpu: "{{ .Values.profiles.mariadbSlave.cpuLimit }}", memory: "{{ .Values.profiles.mariadbSlave.memoryLimit }}" }
ports:
- { containerPort: 3306 }
env:
- { name: MARIADB_ROOT_PASSWORD, valueFrom: { secretKeyRef: { name: "{{ .Values.mariadb }}-secret", key: root-password } } }
readinessProbe:
exec:
command: ["sh","-lc",'mariadb-admin ping -h 127.0.0.1 -uroot -p"$MARIADB_ROOT_PASSWORD" --silent']
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
exec:
command: ["sh","-lc",'mariadb-admin ping -h 127.0.0.1 -uroot -p"$MARIADB_ROOT_PASSWORD" --silent']
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
volumeMounts:
- { name: "{{ .Values.mariadbSlave }}-volume", mountPath: /var/lib/mysql }
- { name: "{{ .Values.mariadbSlave }}-config-volume", mountPath: /etc/mysql/conf.d/replication.cnf, subPath: replication.cnf }
volumes:
- name: "{{ .Values.mariadbSlave }}-volume"
persistentVolumeClaim: { claimName: "{{ .Values.mariadbSlave }}-pvc" }
- name: "{{ .Values.mariadbSlave }}-config-volume"
configMap: { name: "{{ .Values.mariadbSlave }}-config" }
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.mariadbMaster }}", namespace: "{{ .Values.namespace }}" }
spec:
selector: { app: "{{ .Values.mariadbMaster }}" }
ports: [ { name: mysql, protocol: TCP, port: 3306, targetPort: 3306 } ]
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.mariadbSlave }}", namespace: "{{ .Values.namespace }}" }
spec:
selector: { app: "{{ .Values.mariadbSlave }}" }
ports: [ { name: mysql, protocol: TCP, port: 3306, targetPort: 3306 } ]
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.mariadbMaster }}-headless", namespace: "{{ .Values.namespace }}" }
spec:
clusterIP: None
selector: { app: "{{ .Values.mariadbMaster }}" }
ports:
- { name: mysql, protocol: TCP, port: 3306, targetPort: 3306 }
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.mariadbSlave }}-headless", namespace: "{{ .Values.namespace }}" }
spec:
clusterIP: None
selector: { app: "{{ .Values.mariadbSlave }}" }
ports:
- { name: mysql, protocol: TCP, port: 3306, targetPort: 3306 }
{{- end }}
@@ -0,0 +1,128 @@
{{- if .Values.metricHelm }}
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.metric }}-node-exporter", namespace: "{{ .Values.namespace }}" }
spec:
selector: { app: "{{ .Values.metric }}-node-exporter" }
ports: [ { name: metrics, protocol: TCP, port: 9100, targetPort: 9100 } ]
---
apiVersion: apps/v1
kind: DaemonSet
metadata: { name: "{{ .Values.metric }}-node-exporter", namespace: "{{ .Values.namespace }}" }
spec:
selector:
matchLabels: { app: "{{ .Values.metric }}-node-exporter" }
template:
metadata:
labels: { app: "{{ .Values.metric }}-node-exporter" }
spec:
hostNetwork: true
hostPID: true
dnsPolicy: ClusterFirstWithHostNet
tolerations:
- operator: "Exists"
containers:
- name: "{{ .Values.metric }}-node-exporter"
image: quay.io/prometheus/node-exporter:v1.8.2
args:
- --web.listen-address=:9100
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
- --path.rootfs=/host/root
- --collector.textfile.directory={{ .Values.metricPromPath }}
ports: [ { containerPort: 9100, hostPort: 9100, name: metrics } ]
resources:
requests: { cpu: "10m", memory: "32Mi" }
limits: { cpu: "150m", memory: "200Mi" }
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
volumeMounts:
- { name: proc, mountPath: /host/proc, readOnly: true }
- { name: sys, mountPath: /host/sys, readOnly: true }
- { name: root, mountPath: /host/root, readOnly: true }
- { name: textfile, mountPath: "{{ .Values.metricPromPath }}", readOnly: true }
volumes:
- name: proc
hostPath: { path: /proc, type: Directory }
- name: sys
hostPath: { path: /sys, type: Directory }
- name: root
hostPath: { path: /, type: Directory }
- name: textfile
hostPath: { path: "{{ .Values.metricPromPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: ServiceAccount
metadata: { name: "{{ .Values.metric }}-vmagent", namespace: "{{ .Values.namespace }}" }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata: { name: "{{ .Values.metric }}-vmagent" }
rules:
- apiGroups: [""]
resources: ["nodes", "nodes/proxy", "services", "endpoints", "pods"]
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: { name: "{{ .Values.metric }}-vmagent" }
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: "{{ .Values.metric }}-vmagent"
subjects:
- kind: ServiceAccount
name: "{{ .Values.metric }}-vmagent"
namespace: "{{ .Values.namespace }}"
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.metric }}-vmagent", namespace: "{{ .Values.namespace }}" }
spec:
selector: { app: "{{ .Values.metric }}-vmagent" }
ports: [ { name: http, protocol: TCP, port: 8429, targetPort: 8429 } ]
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: "{{ .Values.metric }}-vmagent", namespace: "{{ .Values.namespace }}" }
spec:
replicas: 1
selector:
matchLabels: { app: "{{ .Values.metric }}-vmagent" }
template:
metadata:
labels: { app: "{{ .Values.metric }}-vmagent" }
spec:
serviceAccountName: "{{ .Values.metric }}-vmagent"
containers:
- name: "{{ .Values.metric }}-vmagent"
image: victoriametrics/vmagent:v1.112.0
args:
- -promscrape.config=/etc/vmagent/vmagent.yml
- -httpListenAddr=:8429
- -loggerLevel=INFO
- -remoteWrite.url={{ .Values.metricApiUrl }}
- -remoteWrite.label=server={{ .Values.namespace }}
ports: [ { containerPort: 8429, name: http } ]
resources:
requests: { cpu: "30m", memory: "128Mi" }
limits: { cpu: "300m", memory: "512Mi" }
volumeMounts:
- { name: "{{ .Values.metric }}-vmagent-config-volume", mountPath: /etc/vmagent/vmagent.yml, subPath: vmagent.yml, readOnly: true }
volumes:
- name: "{{ .Values.metric }}-vmagent-config-volume"
hostPath: { path: "{{ .Values.metricVmagentPath }}", type: DirectoryOrCreate }
{{- end }}
@@ -0,0 +1,301 @@
{{- if .Values.openlitespeedHelm }}
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.openlitespeed }}-vhosts-pv" }
spec:
capacity: { storage: 500Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.vhostsPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.openlitespeed }}-vhosts-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.openlitespeed }}-vhosts-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
resources: { requests: { storage: 500Gi } }
storageClassName: ""
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.openlitespeed }}-private-pv" }
spec:
capacity: { storage: 5Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.olsPrivatePath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.openlitespeed }}-private-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.openlitespeed }}-private-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
resources: { requests: { storage: 5Gi } }
storageClassName: ""
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.openlitespeed }}-public-pv" }
spec:
capacity: { storage: 10Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.olsPublicPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.openlitespeed }}-public-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.openlitespeed }}-public-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
resources: { requests: { storage: 10Gi } }
storageClassName: ""
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.openlitespeed }}-config-pv" }
spec:
capacity: { storage: 1Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.olsConfigPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.openlitespeed }}-config-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.openlitespeed }}-config-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
resources: { requests: { storage: 1Gi } }
storageClassName: ""
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.openlitespeed }}-admin-pv" }
spec:
capacity: { storage: 1Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.olsAdminPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.openlitespeed }}-admin-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.openlitespeed }}-admin-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
resources: { requests: { storage: 1Gi } }
storageClassName: ""
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.openlitespeed }}-phpini-pv" }
spec:
capacity: { storage: 1Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.olsPhpIniPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.openlitespeed }}-phpini-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.openlitespeed }}-phpini-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
resources: { requests: { storage: 1Gi } }
storageClassName: ""
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.openlitespeed }}-logs-pv" }
spec:
capacity: { storage: 10Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
hostPath: { path: "{{ .Values.olsLogsPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.openlitespeed }}-logs-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.openlitespeed }}-logs-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
resources: { requests: { storage: 10Gi } }
storageClassName: ""
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: "{{ .Values.openlitespeed }}", namespace: "{{ .Values.namespace }}" }
spec:
replicas: 2
strategy: { type: RollingUpdate, rollingUpdate: { maxSurge: 0, maxUnavailable: 1 } }
selector: { matchLabels: { app: "{{ .Values.openlitespeed }}" } }
template:
metadata: { labels: { app: "{{ .Values.openlitespeed }}" } }
spec:
initContainers:
- name: "{{ .Values.openlitespeed }}-init"
image: litespeedtech/openlitespeed:1.8.5-lsphp85
command: ["sh", "-c"]
args:
- |
if [ ! -f /mnt/config/httpd_config.conf ]; then
cp -a /usr/local/lsws/conf/. /mnt/config/
fi
if [ ! -f /mnt/admin/admin_config.conf ]; then
cp -a /usr/local/lsws/admin/conf/. /mnt/admin/
fi
volumeMounts:
- { name: "{{ .Values.openlitespeed }}-config-volume", mountPath: /mnt/config }
- { name: "{{ .Values.openlitespeed }}-admin-volume", mountPath: /mnt/admin }
shareProcessNamespace: true
containers:
- name: "{{ .Values.openlitespeed }}"
image: litespeedtech/openlitespeed:1.8.5-lsphp85
ports:
- { containerPort: 80 }
- { containerPort: 7080 }
resources:
requests: { cpu: "{{ .Values.profiles.openlitespeed.cpuRequest }}", memory: "{{ .Values.profiles.openlitespeed.memoryRequest }}" }
limits: { cpu: "{{ .Values.profiles.openlitespeed.cpuLimit }}", memory: "{{ .Values.profiles.openlitespeed.memoryLimit }}" }
readinessProbe: { tcpSocket: { port: 80 }, initialDelaySeconds: 5, periodSeconds: 5, failureThreshold: 3 }
livenessProbe: { tcpSocket: { port: 80 }, initialDelaySeconds: 10, periodSeconds: 10, failureThreshold: 5 }
volumeMounts:
- { name: "{{ .Values.openlitespeed }}-vhosts-volume", mountPath: {{ .Values.olsPodVhostsPath }} }
- { name: "{{ .Values.openlitespeed }}-private-volume", mountPath: {{ .Values.olsPodPrivatePath }}, readOnly: true }
- { name: "{{ .Values.openlitespeed }}-public-volume", mountPath: {{ .Values.olsPodPublicPath }}, readOnly: true }
- { name: "{{ .Values.openlitespeed }}-config-volume", mountPath: /usr/local/lsws/conf }
- { name: "{{ .Values.openlitespeed }}-admin-volume", mountPath: /usr/local/lsws/admin/conf }
- { name: "{{ .Values.openlitespeed }}-phpini-volume", mountPath: /etc/ols-php-ini }
- { name: "{{ .Values.openlitespeed }}-logs-volume", mountPath: /usr/local/lsws/logs }
- { name: "{{ .Values.openlitespeed }}-cache-volume", mountPath: /usr/local/lsws/cachedata }
- { name: "{{ .Values.openlitespeed }}-tmp-volume", mountPath: /tmp/lshttpd }
volumes:
- name: "{{ .Values.openlitespeed }}-vhosts-volume"
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-vhosts-pvc" }
- name: "{{ .Values.openlitespeed }}-private-volume"
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-private-pvc" }
- name: "{{ .Values.openlitespeed }}-public-volume"
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-public-pvc" }
- name: "{{ .Values.openlitespeed }}-config-volume"
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-config-pvc" }
- name: "{{ .Values.openlitespeed }}-admin-volume"
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-admin-pvc" }
- name: "{{ .Values.openlitespeed }}-phpini-volume"
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-phpini-pvc" }
- name: "{{ .Values.openlitespeed }}-logs-volume"
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-logs-pvc" }
- name: "{{ .Values.openlitespeed }}-cache-volume"
emptyDir: { sizeLimit: 100Gi }
- name: "{{ .Values.openlitespeed }}-tmp-volume"
emptyDir: { sizeLimit: 100Gi }
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.openlitespeed }}", namespace: "{{ .Values.namespace }}" }
spec:
selector: { app: "{{ .Values.openlitespeed }}" }
ports:
- { name: http, protocol: TCP, port: 80, targetPort: 80 }
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: "{{ .Values.openlitespeed }}-ingress", namespace: "{{ .Values.namespace }}" }
spec:
ingressClassName: traefik
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: "{{ .Values.openlitespeed }}", port: { number: 80 } }
# -------------------------
# Admin panel
# -------------------------
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.openlitespeed }}-admin", namespace: "{{ .Values.namespace }}" }
spec:
selector: { app: "{{ .Values.openlitespeed }}" }
type: ClusterIP
ports:
- { name: admin, protocol: TCP, port: 7080, targetPort: 7080 }
---
apiVersion: traefik.io/v1alpha1
kind: MiddlewareTCP
metadata: { name: "{{ .Values.openlitespeed }}-admin-allowlist", namespace: "{{ .Values.namespace }}" }
spec:
ipAllowList: { sourceRange: [ {{ .Values.olsAdminWhiteList }} ] }
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata: { name: "{{ .Values.openlitespeed }}-admin", namespace: "{{ .Values.namespace }}" }
spec:
entryPoints: [ "olsadmin" ]
routes:
- match: HostSNI(`*`)
middlewares:
- name: "{{ .Values.openlitespeed }}-admin-allowlist"
services:
- name: "{{ .Values.openlitespeed }}-admin"
port: 7080
tls:
passthrough: true
---
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata: { name: traefik, namespace: kube-system }
spec:
valuesContent: |-
ports:
olsadmin:
port: 9443
expose:
default: true
exposedPort: 9443
protocol: TCP
additionalArguments:
- "--entryPoints.olsadmin.address=:9443/tcp"
{{- end }}
@@ -0,0 +1,188 @@
{{- if .Values.postfixHelm }}
---
apiVersion: v1
kind: Secret
metadata: { name: "{{ .Values.postfix }}-tls", namespace: "{{ .Values.namespace }}" }
type: kubernetes.io/tls
data:
tls.crt: {{ .Values.postfixTlsCrtB64 }}
tls.key: {{ .Values.postfixTlsKeyB64 }}
---
apiVersion: v1
kind: ConfigMap
metadata: { name: "{{ .Values.postfix }}-sasl", namespace: "{{ .Values.namespace }}" }
data:
smtpd.conf: |
pwcheck_method: auxprop
auxprop_plugin: sasldb
sasldb_path: /config/sasldb2
mech_list: PLAIN LOGIN
default_realm: {{ .Values.postfixDefaultRealm }}
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.postfix }}-config-pv" }
spec:
capacity: { storage: 1Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteOnce ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.postfixConfigPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.postfix }}-dkim-pv" }
spec:
capacity: { storage: 1Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteOnce ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.postfixDkimPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.postfix }}-config-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.postfix }}-config-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteOnce ]
resources: { requests: { storage: 1Gi } }
storageClassName: ""
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.postfix }}-dkim-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.postfix }}-dkim-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteOnce ]
resources: { requests: { storage: 1Gi } }
storageClassName: ""
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.postfix }}-queue-pvc", namespace: "{{ .Values.namespace }}" }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 5Gi } }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: "{{ .Values.postfix }}", namespace: "{{ .Values.namespace }}" }
spec:
replicas: 1
selector: { matchLabels: { app: "{{ .Values.postfix }}" } }
template:
metadata: { labels: { app: "{{ .Values.postfix }}" } }
spec:
enableServiceLinks: false
initContainers:
- name: "{{ .Values.postfix }}-dkim-init"
image: boky/postfix:4.4.0-alpine
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-lc"]
args:
- |
set -e
if [ ! -f /dkim/opendkim.conf ]; then
cp -a /etc/opendkim/* /dkim/
fi
touch /dkim/TrustedHosts /dkim/SigningTable /dkim/KeyTable
chown opendkim:opendkim /dkim/TrustedHosts /dkim/KeyTable /dkim/SigningTable
chmod 0644 /dkim/TrustedHosts /dkim/KeyTable /dkim/SigningTable
printf '%s\n' 127.0.0.1 localhost 10.42.0.0/16 10.43.0.0/16 > /dkim/TrustedHosts
volumeMounts:
- name: "{{ .Values.postfix }}-dkim-volume"
mountPath: /dkim
containers:
- name: "{{ .Values.postfix }}"
image: boky/postfix:4.4.0-alpine
ports:
- { containerPort: 25, name: smtp }
- { containerPort: 587, name: submission }
env:
- { name: POSTFIX_myhostname, value: "{{ .Values.postfixHost }}" }
- { name: POSTFIX_smtpd_banner, value: "$myhostname ESMTP" }
- { name: POSTFIX_mynetworks, value: "127.0.0.0/8" }
- { name: POSTFIX_inet_interfaces, value: "all" }
# Rules
- { name: POSTFIX_smtpd_client_restrictions, value: "permit_mynetworks, permit_sasl_authenticated, reject" }
- { name: POSTFIX_smtpd_relay_restrictions, value: "permit_sasl_authenticated, reject_unauth_destination" }
- { name: POSTFIX_smtpd_sender_restrictions, value: "reject_non_fqdn_sender,reject_unknown_sender_domain,reject_sender_login_mismatch,permit_sasl_authenticated,reject_unauth_destination" }
# TLS
- { name: POSTFIX_smtpd_tls_cert_file, value: "/etc/ssl/mail/tls.crt" }
- { name: POSTFIX_smtpd_tls_key_file, value: "/etc/ssl/mail/tls.key" }
- { name: POSTFIX_smtpd_tls_security_level, value: "may" }
- { name: POSTFIX_smtp_tls_security_level, value: "may" }
# SASL (Cyrus, sasldb2)
- { name: POSTFIX_smtpd_sasl_auth_enable, value: "yes" }
- { name: POSTFIX_smtpd_sasl_type, value: "cyrus" }
- { name: POSTFIX_smtpd_sasl_path, value: "smtpd" }
- { name: POSTFIX_cyrus_sasl_config_path, value: "/etc/sasl2" }
# Maps (Virtual domain/alias and senders)
- { name: POSTFIX_virtual_alias_domains, value: "lmdb:/config/{{ .Values.postfixDomainsFile }}" }
- { name: POSTFIX_virtual_alias_maps, value: "lmdb:/config/{{ .Values.postfixAliasesFile }}" }
- { name: POSTFIX_smtpd_sender_login_maps, value: "lmdb:/config/{{ .Values.postfixSendersFile }}" }
# DKIM
- { name: DKIM_SELECTOR, value: "{{ .Values.postfixDkimSelector }}" }
- { name: POSTFIX_smtpd_milters, value: "inet:localhost:8891" }
- { name: POSTFIX_non_smtpd_milters, value: "$smtpd_milters" }
- { name: POSTFIX_milter_default_action, value: "accept" }
- { name: POSTFIX_milter_protocol, value: "6" }
# Other
- { name: ALLOW_EMPTY_SENDER_DOMAINS, value: "true" }
- { name: ALLOWED_SENDER_DOMAINS, value: "" }
volumeMounts:
- { name: "{{ .Values.postfix }}-tls-volume", mountPath: /etc/ssl/mail, readOnly: true }
- { name: "{{ .Values.postfix }}-sasl-volume", mountPath: /etc/sasl2 }
- { name: "{{ .Values.postfix }}-config-volume", mountPath: /config }
- { name: "{{ .Values.postfix }}-dkim-volume", mountPath: /etc/opendkim }
- { name: "{{ .Values.postfix }}-dkim-volume", mountPath: /etc/opendkim/keys, subPath: keys }
- { name: "{{ .Values.postfix }}-queue-volume", mountPath: /var/spool/postfix }
volumes:
- name: "{{ .Values.postfix }}-tls-volume"
secret: { secretName: "{{ .Values.postfix }}-tls" }
- name: "{{ .Values.postfix }}-sasl-volume"
configMap: { name: "{{ .Values.postfix }}-sasl" }
- name: "{{ .Values.postfix }}-config-volume"
persistentVolumeClaim: { claimName: "{{ .Values.postfix }}-config-pvc" }
- name: "{{ .Values.postfix }}-dkim-volume"
persistentVolumeClaim: { claimName: "{{ .Values.postfix }}-dkim-pvc" }
- name: "{{ .Values.postfix }}-queue-volume"
persistentVolumeClaim: { claimName: "{{ .Values.postfix }}-queue-pvc" }
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.postfix }}-public", namespace: "{{ .Values.namespace }}" }
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector: { app: "{{ .Values.postfix }}" }
ports: [ { name: smtp, port: 25, targetPort: 25 } ]
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.postfix }}", namespace: "{{ .Values.namespace }}" }
spec:
selector: { app: "{{ .Values.postfix }}" }
ports: [ { name: submission, port: 587, targetPort: 587 } ]
{{- end }}
@@ -0,0 +1,416 @@
{{- if .Values.redisHelm }}
---
apiVersion: v1
kind: ConfigMap
metadata: { name: "{{ .Values.redis }}-config", namespace: "{{ .Values.namespace }}" }
data:
redis.conf: |
bind 0.0.0.0
port 6379
dir /data
# --- ACL ---
aclfile /data-acl/{{ .Values.redisFileUsersAcl }}
# --- all in one DB ---
databases 1
# --- network ---
protected-mode yes
tcp-backlog 1024
tcp-keepalive 300
timeout 0
# --- connections ---
maxclients {{ .Values.profiles.redis.maxClients }}
# --- memory (tune) ---
maxmemory {{ .Values.profiles.redis.maxmemory }}
maxmemory-policy allkeys-lfu
maxmemory-samples 5
maxmemory-eviction-tenacity 10
maxmemory-clients 5%
client-query-buffer-limit 256mb
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
# --- replication ---
replica-serve-stale-data yes
replica-read-only yes
repl-backlog-size 64mb
repl-backlog-ttl 3600
min-replicas-to-write 0
#min-replicas-max-lag 10
# --- persistence ---
appendonly yes
appendfsync everysec
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
save ""
# --- log ---
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
---
apiVersion: v1
kind: PersistentVolume
metadata: { name: "{{ .Values.redis }}-users-pv" }
spec:
capacity: { storage: 1Gi }
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: "{{ .Values.redisPath }}", type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: "{{ .Values.redis }}-users-pvc", namespace: "{{ .Values.namespace }}" }
spec:
volumeName: "{{ .Values.redis }}-users-pv"
volumeMode: Filesystem
accessModes: [ ReadWriteMany ]
resources: { requests: { storage: 1Gi } }
storageClassName: ""
---
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: "{{ .Values.redis }}", namespace: "{{ .Values.namespace }}" }
spec:
serviceName: "{{ .Values.redis }}"
replicas: 2
selector: { matchLabels: { app: "{{ .Values.redis }}" } }
persistentVolumeClaimRetentionPolicy: { whenDeleted: Delete, whenScaled: Retain }
template:
metadata: { labels: { app: "{{ .Values.redis }}" } }
spec:
terminationGracePeriodSeconds: 30
initContainers:
- name: init-redis-acl
image: redis:8.6-alpine
resources:
requests: { cpu: "10m", memory: "32Mi" }
limits: { cpu: "100m", memory: "128Mi" }
env:
- { name: POD_NAME, valueFrom: { fieldRef: { fieldPath: metadata.name } } }
- { name: REDIS_PASSWORD, valueFrom: { secretKeyRef: { name: "{{ .Values.redis }}-secret", key: root-password } } }
command: ["/bin/sh","-c"]
args:
- |
set -eu
ACL="/data-acl/{{ .Values.redisFileUsersAcl }}"
HASH=$(printf '%s' "$REDIS_PASSWORD" | sha256sum | awk '{print $1}')
mkdir -p /data-acl
if [ "$POD_NAME" = "{{ .Values.redis }}-0" ]; then
tmp="${ACL}.tmp"
if [ -f "$ACL" ]; then
awk '!(tolower($1)=="user" && $2=="default")' "$ACL" > "$tmp"
else
: > "$tmp"
fi
# default user is used by replication auth and HAProxy health checks
printf 'user default on sanitize-payload #%s ~* &* +@all\n' "$HASH" >> "$tmp"
mv "$tmp" "$ACL"
chmod 600 "$ACL"
else
i=0
while [ ! -f "$ACL" ] && [ $i -lt 300 ]; do
sleep 1
i=$((i+1))
done
[ -f "$ACL" ] || exit 1
fi
volumeMounts:
- { name: "{{ .Values.redis }}-users-volume", mountPath: /data-acl }
containers:
- name: "{{ .Values.redis }}"
image: redis:8.6-alpine
resources:
requests: { cpu: "{{ .Values.profiles.redis.cpuRequest }}", memory: "{{ .Values.profiles.redis.memoryRequest }}" }
limits: { cpu: "{{ .Values.profiles.redis.cpuLimit }}", memory: "{{ .Values.profiles.redis.memoryLimit }}" }
ports:
- { name: redis, containerPort: 6379 }
env:
- { name: POD_NAME, valueFrom: { fieldRef: { fieldPath: metadata.name } } }
- { name: POD_IP, valueFrom: { fieldRef: { fieldPath: status.podIP } } }
- { name: REDIS_PASSWORD, valueFrom: { secretKeyRef: { name: "{{ .Values.redis }}-secret", key: root-password } } }
- { name: SENTINEL_HOST, value: "{{ .Values.redisSentinel }}" }
- { name: SENTINEL_PORT, value: "26379" }
- { name: MASTER_NAME, value: "mymaster" }
command: ["/bin/sh","-c"]
args:
- |
set -eu
POD_DNS_SHORT="${POD_NAME}.{{ .Values.redis }}"
POD_DNS_FQDN="${POD_NAME}.{{ .Values.redis }}.{{ .Values.namespace }}.svc.cluster.local"
get_master() {
REDISCLI_AUTH="$REDIS_PASSWORD" \
redis-cli -h "$SENTINEL_HOST" -p "$SENTINEL_PORT" \
SENTINEL get-master-addr-by-name "$MASTER_NAME" 2>/dev/null | tr -d '\r'
}
out=""
i=0
while [ $i -lt 20 ]; do
out="$(get_master || true)"
[ -n "$out" ] && break
i=$((i+1))
sleep 1
done
master_host="$(printf '%s\n' "$out" | sed -n '1p')"
master_port="$(printf '%s\n' "$out" | sed -n '2p')"
[ -n "$master_port" ] || master_port="6379"
is_me_master() {
[ "$master_host" = "$POD_IP" ] || [ "$master_host" = "$POD_DNS_SHORT" ] || [ "$master_host" = "$POD_DNS_FQDN" ]
}
if [ -n "$master_host" ]; then
if is_me_master; then
exec redis-server /etc/redis/redis.conf --masteruser default --masterauth "$REDIS_PASSWORD"
else
exec redis-server /etc/redis/redis.conf --replicaof "$master_host" "$master_port" --masteruser default --masterauth "$REDIS_PASSWORD"
fi
else
# bootstrap if sentinel is not ready yet
if [ "$POD_NAME" = "{{ .Values.redis }}-0" ]; then
exec redis-server /etc/redis/redis.conf
else
exec redis-server /etc/redis/redis.conf --replicaof {{ .Values.redis }}-0.{{ .Values.redis }} 6379 --masteruser default --masterauth "$REDIS_PASSWORD"
fi
fi
volumeMounts:
- { name: "{{ .Values.redis }}-data-volume", mountPath: /data }
- { name: "{{ .Values.redis }}-config-volume", mountPath: /etc/redis }
- { name: "{{ .Values.redis }}-users-volume", mountPath: /data-acl }
volumes:
- name: "{{ .Values.redis }}-config-volume"
configMap: { name: "{{ .Values.redis }}-config" }
- name: "{{ .Values.redis }}-users-volume"
persistentVolumeClaim: { claimName: "{{ .Values.redis }}-users-pvc" }
volumeClaimTemplates:
- metadata: { name: "{{ .Values.redis }}-data-volume" }
spec:
accessModes: [ ReadWriteOnce ]
resources: { requests: { storage: 10Gi } }
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.redis }}", namespace: "{{ .Values.namespace }}" }
spec:
clusterIP: None
selector: { app: "{{ .Values.redis }}" }
ports:
- { name: redis, protocol: TCP, port: 6379, targetPort: 6379 }
# -------------------------
# Sentinel (1) with PVC
# -------------------------
---
apiVersion: v1
kind: ConfigMap
metadata: { name: "{{ .Values.redisSentinel }}-config", namespace: "{{ .Values.namespace }}" }
data:
sentinel.conf.tmpl: |
bind 0.0.0.0
port 26379
dir /data
sentinel deny-scripts-reconfig yes
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
# quorum=1 (single sentinel)
sentinel monitor mymaster {{ .Values.redis }}-0.{{ .Values.redis }} 6379 1
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel auth-user mymaster default
sentinel auth-pass mymaster __PASSWORD__
requirepass __PASSWORD__
---
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: "{{ .Values.redisSentinel }}", namespace: "{{ .Values.namespace }}" }
spec:
replicas: 1
serviceName: "{{ .Values.redisSentinel }}"
selector: { matchLabels: { app: "{{ .Values.redisSentinel }}" } }
persistentVolumeClaimRetentionPolicy: { whenDeleted: Delete, whenScaled: Retain }
template:
metadata: { labels: { app: "{{ .Values.redisSentinel }}" } }
spec:
containers:
- name: "{{ .Values.redisSentinel }}"
image: redis:8.6-alpine
resources:
requests: { cpu: "50m", memory: "128Mi" }
limits: { cpu: "200m", memory: "256Mi" }
ports:
- { name: sentinel, containerPort: 26379 }
env:
- { name: REDIS_PASSWORD, valueFrom: { secretKeyRef: { name: "{{ .Values.redis }}-secret", key: root-password } } }
command: ["/bin/sh","-c"]
args:
- |
set -eu
CONF=/data/sentinel.conf
if [ ! -f "$CONF" ]; then
pwd_escaped=$(printf '%s' "$REDIS_PASSWORD" | sed -e 's/[\/&]/\\&/g')
sed "s/__PASSWORD__/${pwd_escaped}/g" /tmpl/sentinel.conf.tmpl > "$CONF"
chmod 600 "$CONF"
fi
exec redis-server "$CONF" --sentinel
volumeMounts:
- { name: "{{ .Values.redisSentinel }}-tmpl", mountPath: /tmpl }
- { name: "{{ .Values.redisSentinel }}-data", mountPath: /data }
volumes:
- name: "{{ .Values.redisSentinel }}-tmpl"
configMap: { name: "{{ .Values.redisSentinel }}-config" }
volumeClaimTemplates:
- metadata: { name: "{{ .Values.redisSentinel }}-data" }
spec:
accessModes: [ ReadWriteOnce ]
resources: { requests: { storage: 1Gi } }
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.redisSentinel }}", namespace: "{{ .Values.namespace }}" }
spec:
selector: { app: "{{ .Values.redisSentinel }}" }
ports:
- { name: sentinel, port: 26379, targetPort: 26379 }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: "{{ .Values.redisSentinel }}-allow-only-checker", namespace: "{{ .Values.namespace }}" }
spec:
podSelector: { matchLabels: { app: "{{ .Values.redisSentinel }}" } }
policyTypes:
- Ingress
ingress:
- from:
- podSelector: { matchLabels: { app: "{{ .Values.redis }}" } }
- podSelector: { matchLabels: { app: "{{ .Values.redisSentinel }}" } }
ports:
- { protocol: TCP, port: 26379 }
# -------------------------
# HAProxy: single master endpoint
# -------------------------
---
apiVersion: v1
kind: ConfigMap
metadata: { name: "{{ .Values.redis }}-haproxy-config", namespace: "{{ .Values.namespace }}" }
data:
haproxy.cfg: |
global
log stdout format raw local0
maxconn 20000
resolvers kubedns
nameserver dns1 10.43.0.10:53
accepted_payload_size 8192
resolve_retries 3
timeout resolve 1s
timeout retry 1s
hold valid 10s
hold obsolete 30s
defaults
mode tcp
log global
option tcplog
option log-health-checks
timeout connect 5s
timeout client 1m
timeout server 1m
timeout check 1s
frontend fe_redis_master
bind *:6379
default_backend be_redis_master
backend be_redis_master
mode tcp
balance first
option tcp-check
option srvtcpka
timeout queue 2s
timeout connect 2s
timeout check 3s
timeout server 10m
tcp-check connect
tcp-check send-lf "AUTH default $REDIS_PASSWORD\r\n"
tcp-check expect string +OK
tcp-check send INFO\ replication\r\n
tcp-check expect string role:master
tcp-check send QUIT\r\n
tcp-check expect string +OK
server redis0 {{ .Values.redis }}-0.{{ .Values.redis }}.{{ .Values.namespace }}.svc.cluster.local:6379 check resolvers kubedns resolve-prefer ipv4 init-addr libc,none inter 5s fall 2 rise 2
server redis1 {{ .Values.redis }}-1.{{ .Values.redis }}.{{ .Values.namespace }}.svc.cluster.local:6379 check resolvers kubedns resolve-prefer ipv4 init-addr libc,none inter 5s fall 2 rise 2
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: "{{ .Values.redis }}-haproxy", namespace: "{{ .Values.namespace }}" }
spec:
replicas: 1
selector: { matchLabels: { app: "{{ .Values.redis }}-haproxy" } }
template:
metadata:
labels: { app: "{{ .Values.redis }}-haproxy" }
spec:
containers:
- name: "{{ .Values.redis }}-haproxy"
image: haproxy:2.9-alpine
resources:
requests: { cpu: "50m", memory: "64Mi" }
limits: { cpu: "500m", memory: "256Mi" }
ports:
- { name: redis, containerPort: 6379 }
env:
- { name: REDIS_PASSWORD, valueFrom: { secretKeyRef: { name: "{{ .Values.redis }}-secret", key: root-password } } }
command: ["/bin/sh","-c"]
args:
- |
set -eu
haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
exec haproxy -f /usr/local/etc/haproxy/haproxy.cfg -db
readinessProbe: { tcpSocket: { port: 6379 }, initialDelaySeconds: 1, periodSeconds: 2, failureThreshold: 3 }
livenessProbe: { tcpSocket: { port: 6379 }, initialDelaySeconds: 10, periodSeconds: 10, failureThreshold: 3 }
volumeMounts:
# - { name: cfg, mountPath: /usr/local/etc/haproxy/haproxy.cfg, subPath: haproxy.cfg }
- { name: cfg, mountPath: /usr/local/etc/haproxy }
volumes:
- name: cfg
configMap: { name: "{{ .Values.redis }}-haproxy-config" }
---
apiVersion: v1
kind: Service
metadata: { name: "{{ .Values.redis }}-master", namespace: "{{ .Values.namespace }}" }
spec:
selector: { app: "{{ .Values.redis }}-haproxy" }
ports:
- { name: redis, port: 6379, targetPort: 6379 }
{{- end }}
@@ -0,0 +1,52 @@
{{- if .Values.workerHelm }}
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: "{{ .Values.worker }}", namespace: "{{ .Values.namespace }}" }
spec:
replicas: 1
selector: { matchLabels: { app: "{{ .Values.worker }}" } }
template:
metadata: { labels: { app: "{{ .Values.worker }}" } }
spec:
containers:
- name: "{{ .Values.worker }}"
image: python:3.12-slim
imagePullPolicy: IfNotPresent
resources:
requests: { cpu: "50m", memory: "64Mi" }
limits: { cpu: "200m", memory: "256Mi" }
ports:
- { containerPort: 8080 }
workingDir: /app/agent
command: ["/bin/sh","-c"]
args:
- |
set -e
if [ ! -f /app/agent/worker.py ]; then
echo "ERROR: /app/agent/worker.py not found" >&2
ls -la /app/agent >&2 || true
exit 1
fi
exec python -u /app/agent/worker.py
env:
- { name: WORKER_UUID, value: "{{ .Values.workerUuid }}" }
- { name: WORKER_NAME, value: "{{ .Values.workerName }}" }
- { name: WORKER_POOL, value: "{{ .Values.workerPool }}" }
- { name: API_URL, value: "{{ .Values.workerApiUrl }}" }
- { name: GETTING_PAUSE, value: "{{ .Values.workerApiGettingPause }}" }
- { name: SENDING_PAUSE, value: "{{ .Values.workerApiSendingPause }}" }
volumeMounts:
- { name: "{{ .Values.worker }}-agent-volume", mountPath: /app/agent }
- { name: "{{ .Values.worker }}-tasks-volume", mountPath: /app/tasks }
readinessProbe: { httpGet: { path: /healthz, port: 8080 }, periodSeconds: 5, timeoutSeconds: 2 }
livenessProbe: { httpGet: { path: /healthz, port: 8080 }, periodSeconds: 10, timeoutSeconds: 2, failureThreshold: 3 }
startupProbe: { httpGet: { path: /healthz, port: 8080 }, periodSeconds: 2, timeoutSeconds: 2, failureThreshold: 30 }
volumes:
- name: "{{ .Values.worker }}-agent-volume"
hostPath: { path: "{{ .Values.workerAgentPath }}", type: DirectoryOrCreate }
- name: "{{ .Values.worker }}-tasks-volume"
hostPath: { path: "{{ .Values.workerTasksPath }}", type: DirectoryOrCreate }
{{- end }}
+42
View File
@@ -0,0 +1,42 @@
global:
scrape_interval: 15s
scrape_timeout: 10s
scrape_configs:
- job_name: "metric-node-exporter"
kubernetes_sd_configs:
- role: pod
relabel_configs:
- action: keep
source_labels: [__meta_kubernetes_pod_label_app]
regex: metric-node-exporter
- action: keep
source_labels: [__meta_kubernetes_pod_container_port_number]
regex: "9100"
- action: replace
source_labels: [__meta_kubernetes_pod_node_name]
target_label: node
- job_name: "metric-kubelet-cadvisor"
scheme: https
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
tls_config:
insecure_skip_verify: true
kubernetes_sd_configs:
- role: node
relabel_configs:
- target_label: __address__
replacement: kubernetes.default.svc:443
- source_labels: [__meta_kubernetes_node_name]
target_label: __metrics_path__
replacement: /api/v1/nodes/$1/proxy/metrics/cadvisor
- source_labels: [__meta_kubernetes_node_name]
target_label: node
metric_relabel_configs:
- source_labels: [__name__]
action: keep
regex: 'container_memory_working_set_bytes|container_memory_usage_bytes|container_memory_rss|container_memory_cache|container_cpu_usage_seconds_total|container_cpu_system_seconds_total|container_cpu_user_seconds_total'
+15
View File
@@ -0,0 +1,15 @@
From: "Monitoring" <postmaster@mta.krumax.cz>
To: Maksym <maksym.krugol@wedos.org>
Subject: Test message (RFC822 raw)
Date: Thu, 05 Feb 2026 10:15:00 +0100
Message-ID: <test-20260205-101500.1@mta.krumax.cz>
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Hello!
This is a raw RFC822 message file sent via sendmail -t.
Regards,
Monitoring
@@ -0,0 +1,52 @@
expose_php = Off
allow_url_fopen = Off
allow_url_include = Off
enable_dl = Off
short_open_tag = Off
; --- block user_ini ---
user_ini.filename =
; -- disable functions ---
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,putenv,dl,show_source,highlight_file,symlink,readlink,link,proc_terminate,proc_get_status,pfsockopen,posix_kill,posix_setuid,posix_setgid,posix_setsid,posix_setpgid,posix_getgrnam,posix_getpgid,posix_getpwuid,posix_getpwnam,posix_getrlimit,posix_initgroups,posix_mkfifo,posix_mknod,posix_uname,register_tick_function,openlog,syslog,proc_close,proc_nice,diskfreespace,disk_free_space,disk_total_space,leak,pcntl_alarm,pcntl_async_signals,pcntl_exec,pcntl_fork,pcntl_get_last_error,pcntl_getcpuaffinity,pcntl_getpriority,pcntl_rfork,pcntl_setcpuaffinity,pcntl_setpriority,pcntl_signal,pcntl_signal_dispatch,pcntl_signal_get_handler,pcntl_sigprocmask,pcntl_sigtimedwait,pcntl_sigwaitinfo,pcntl_strerror,pcntl_unshare,pcntl_wait,pcntl_waitid,pcntl_waitpid,pcntl_wexitstatus,pcntl_wifexited,pcntl_wifsignaled,pcntl_wifstopped,pcntl_wstopsig,pcntl_wtermsig,sys_getloadavg
; not use for LSAPI
;pm.max_children = {{children}}
; --- memory limit ---
max_memory_limit = {{max_memory_limit}}
; --- default (redefined per vhost) ---
memory_limit = {{memory_limit}}
max_execution_time = {{max_execution_time}}
max_input_time = 30
max_input_vars = 1000
output_buffering = 4096
; --- uploads ---
post_max_size = {{post_max_size}}
upload_max_filesize = {{upload_max_filesize}}
max_file_uploads = 10
; --- log --- (to stderr k3s?)
display_errors = Off
log_errors = On
;error_log = /usr/local/lsws/conf/logs/php_errors.log
error_log = /proc/self/fd/2
; --- sessions (redefined per vhost) ---
session.save_path = "/tmp"
session.use_only_cookies = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
; --- OPcache ---
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 100000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 2
opcache.jit = "disable"
@@ -0,0 +1,6 @@
children=16
max_memory_limit=512M
memory_limit=512M
max_execution_time=30
post_max_size=512M
upload_max_filesize=512M
@@ -0,0 +1,6 @@
children=4
max_memory_limit=512M
memory_limit=512M
max_execution_time=30
post_max_size=128M
upload_max_filesize=128M
@@ -0,0 +1,6 @@
children=6
max_memory_limit=512M
memory_limit=512M
max_execution_time=30
post_max_size=128M
upload_max_filesize=128M
@@ -0,0 +1,6 @@
children=8
max_memory_limit=512M
memory_limit=512M
max_execution_time=30
post_max_size=128M
upload_max_filesize=128M
@@ -0,0 +1,8 @@
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Front-controller
RewriteRule ^/?index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
@@ -0,0 +1,11 @@
RewriteEngine On
# no www -> add www + https
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# www, http -> https
RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC]
RewriteCond %{HTTP:Forwarded} !proto=https [NC]
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
@@ -0,0 +1,11 @@
RewriteEngine On
# www -> non-www + https
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
# http -> https
RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC]
RewriteCond %{HTTP:Forwarded} !proto=https [NC]
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
@@ -0,0 +1,7 @@
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Laravel
RewriteRule . /public/index.php [L]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
@@ -0,0 +1,13 @@
RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}/.php81 -f
RewriteRule ^ - [H=application/x-httpd-lsphp81]
RewriteCond %{DOCUMENT_ROOT}/.php82 -f
RewriteRule ^ - [H=application/x-httpd-lsphp82]
RewriteCond %{DOCUMENT_ROOT}/.php83 -f
RewriteRule ^ - [H=application/x-httpd-lsphp83]
RewriteCond %{DOCUMENT_ROOT}/.php84 -f
RewriteRule ^ - [H=application/x-httpd-lsphp84]
@@ -0,0 +1,14 @@
RewriteEngine On
# Unigma 1.0.0 fix
RewriteCond %{REQUEST_URI} ^/v(8[1-5])/ [OR]
RewriteCond %{REQUEST_URI} ^/v(8[1-5])/router\.lua [OR]
RewriteCond %{REQUEST_URI} ^/router\.lua
RewriteRule ^ - [L]
RewriteCond %{REQUEST_URI} !^/router\.lua
RewriteCond %{REQUEST_URI} !^/[^/]+/www/
RewriteCond %{HTTP_HOST} ^([a-z0-9.-]+)$
RewriteRule ^/?(.*)$ /%1/www/$1
RewriteRule ^/?(.*\.php)$ /router.lua?orig=/$1 [L]
@@ -0,0 +1,37 @@
allowSymbolLink 1
enableScript 1
restrained 1
setUIDMode 2
vhRoot /var/www/vhosts/$VH_NAME
virtualHostConfig {
docRoot $VH_ROOT/www/
vhDomain $VH_NAME
vhAliases *.$VH_NAME
index {
useServer 0
indexFiles index.php
}
phpIniOverride {
php_admin_value upload_tmp_dir $VH_ROOT/tmp
php_admin_value session.save_path $VH_ROOT/session
php_admin_value open_basedir "$VH_ROOT/www:$VH_ROOT/tmp:$VH_ROOT/session:/var/www/private/$VH_NAME:/var/www/public"
php_admin_value auto_prepend_file /var/www/private/$VH_NAME/bootstrap.php
}
rewrite {
enable 1
autoLoadHtaccess 1
rules <<<END_rules
#rewriteFile /usr/local/lsws/conf/rules/php.rules
rewriteFile /usr/local/lsws/conf/rules/https.rules
rewriteFile /usr/local/lsws/conf/rules/front-controller.rules
END_rules
}
accessControl {
allow 127.0.0.1, ::1, 10.42.0.0/16, 10.43.0.0/16
}
}
@@ -0,0 +1,38 @@
docRoot /var/www/vhosts/{{domain}}/www/
vhDomain {{domain}}
vhAliases *.{{domain}}
index {
useServer 0
indexFiles index.php
}
phpIniOverride {
# --- paths ---
php_admin_value upload_tmp_dir /var/www/vhosts/{{domain}}/tmp
php_admin_value session.save_path /var/www/vhosts/{{domain}}/session
php_admin_value open_basedir "/var/www/vhosts/{{domain}}/www:/var/www/vhosts/{{domain}}/tmp:/var/www/vhosts/{{domain}}/session:/var/www/data/{{domain}}:/var/www/shared"
php_admin_value auto_prepend_file /var/www/data/{{domain}}/bootstrap.php
# --- hard limits ---
php_admin_value memory_limit {{memory_limit}}
php_admin_value max_execution_time {{max_execution_time}}
php_admin_value max_input_time 30
php_admin_value max_input_vars 1000
php_admin_value post_max_size {{post_max_size}}
php_admin_value upload_max_filesize {{upload_max_filesize}}
}
rewrite {
enable 1
autoLoadHtaccess 1
rules <<<END_rules
rewriteFile /usr/local/lsws/conf/rules/php.rules
rewriteFile /usr/local/lsws/conf/rules/https.rules
rewriteFile /usr/local/lsws/conf/rules/front-controller.rules
END_rules
}
accessControl {
allow 127.0.0.1, ::1, 10.42.0.0/16, 10.43.0.0/16
}
@@ -0,0 +1,38 @@
docRoot $VH_ROOT/www/
vhDomain $VH_NAME
vhAliases *.$VH_NAME
index {
useServer 0
indexFiles index.php
}
phpIniOverride {
# --- paths ---
php_admin_value upload_tmp_dir $VH_ROOT/tmp
php_admin_value session.save_path $VH_ROOT/session
php_admin_value open_basedir "$VH_ROOT/www:$VH_ROOT/tmp:$VH_ROOT/session:/var/www/private/$VH_NAME:/var/www/public"
php_admin_value auto_prepend_file /var/www/private/$VH_NAME/bootstrap.php
# --- hard limits ---
php_admin_value memory_limit {{memory_limit}}
php_admin_value max_execution_time {{max_execution_time}}
php_admin_value max_input_time 30
php_admin_value max_input_vars 1000
php_admin_value post_max_size {{post_max_size}}
php_admin_value upload_max_filesize {{upload_max_filesize}}
}
rewrite {
enable 1
autoLoadHtaccess 1
rules <<<END_rules
rewriteFile /usr/local/lsws/conf/rules/php.rules
rewriteFile /usr/local/lsws/conf/rules/https.rules
rewriteFile /usr/local/lsws/conf/rules/front-controller.rules
END_rules
}
accessControl {
allow 127.0.0.1, ::1, 10.42.0.0/16, 10.43.0.0/16
}
@@ -0,0 +1,23 @@
# OLS
aliasLimit=0
phpChildren=16
maxExecutionTime=30
maxMemoryLimit=512M
memoryLimit=512M
postMaxSize=512M
uploadMaxFilesize=512M
# Filesystem
blockSoftLimit=0
blockHardLimit=0
inodeSoftLimit=0
inodeHardLimit=0
# Database
databaseSoftLimit=0
# Backup 1/7
backupPeriod=7
# Mail 50/500
mailDayLimit=0
@@ -0,0 +1,23 @@
# OLS
aliasLimit=0
phpChildren=16
maxExecutionTime=30
maxMemoryLimit=512M
memoryLimit=512M
postMaxSize=512M
uploadMaxFilesize=512M
# Filesystem
blockSoftLimit=0
blockHardLimit=0
inodeSoftLimit=0
inodeHardLimit=0
# Database
databaseSoftLimit=0
# Backup 1/7
backupPeriod=7
# Mail 50/500
mailDayLimit=0
@@ -0,0 +1,23 @@
# OLS
aliasLimit=0
phpChildren=16
maxExecutionTime=30
maxMemoryLimit=512M
memoryLimit=512M
postMaxSize=512M
uploadMaxFilesize=512M
# Filesystem
blockSoftLimit=0
blockHardLimit=0
inodeSoftLimit=0
inodeHardLimit=0
# Database
databaseSoftLimit=0
# Backup 1/7
backupPeriod=7
# Mail 50/500
mailDayLimit=0
@@ -0,0 +1,19 @@
[sshd]
enabled = true
backend = systemd
maxretry = 4
findtime = 600
bantime = 3600
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 604800
[recidive]
enabled = true
backend = systemd
filter = recidive
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 604800
findtime = 86400
maxretry = 3
@@ -0,0 +1,20 @@
# --- KUBE SFTP GLOBAL BEGIN ---
PermitRootLogin no
PermitUserEnvironment no
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 5
MaxStartups 200:30:500
Compression no
DebianBanner no
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
PubkeyAcceptedAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
# --- KUBE SFTP GLOBAL END ---
@@ -0,0 +1,22 @@
# --- KUBE SFTP GROUP BEGIN ---
Match Group {{sftp_access_group}}
ChrootDirectory %h
ForceCommand internal-sftp -d /www -u 027
PasswordAuthentication yes
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitTTY no
PermitTunnel no
AllowTcpForwarding no
AllowAgentForwarding no
AllowStreamLocalForwarding no
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
# --- KUBE SFTP GROUP END ---
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
<?php
echo '<pre>pid: ' . getmypid() . '</pre>';
echo phpinfo();
Binary file not shown.
@@ -0,0 +1,8 @@
<?php
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
echo "time: <b>" . time() . "</b> | hostname: <b>" . gethostname() . "</b> | pid: <b>" . getmypid() . "</b><br>";
phpinfo();
+136
View File
@@ -0,0 +1,136 @@
<?php
require __DIR__ . '/wp-load.php';
define('MAIL_TEST_KEY', 'dev');
$smtpLog = '';
add_action('phpmailer_init', function ($phpmailer) use (&$smtpLog) {
$phpmailer->SMTPDebug = 2;
$phpmailer->Debugoutput = function ($str, $level) use (&$smtpLog) {
$smtpLog .= date('Y-m-d H:i:s') . ' ' . htmlentities(preg_replace('/[\r\n]+/', '', $str), ENT_QUOTES, 'UTF-8') . "<br>\n";
};
});
$success = '';
$error = '';
$err = null;
add_action('wp_mail_failed', function($e) use (&$err) {
if (is_wp_error($e)) {
$err = $e->get_error_message();
} else {
$err = 'Unknown wp_mail error';
}
});
$domain = wp_parse_url(home_url(), PHP_URL_HOST);
$to = "maksym.krugol@wedos.org";
$headers = [
"List-Unsubscribe: <mailto:unsubscribe@{$domain}?subject=unsubscribe>, <https://{$domain}/unsubscribe/{$to}>"
];
$subject = "Service status update and new API keys - " . (new DateTime())->format('d.m.Y');
$message = '';
$message .= "Service: host-" . bin2hex(random_bytes(2)) . PHP_EOL;
$message .= "Status: active" . PHP_EOL;
$message .= "Service tag: " . bin2hex(random_bytes(6)) . PHP_EOL . PHP_EOL;
for ($i = 1; $i < 9; $i++) {
$message .= "API key" . $i . ": " . bin2hex(random_bytes(40)) . PHP_EOL;
}
$message .= PHP_EOL . "Thank you for your understanding." . PHP_EOL . PHP_EOL . "Support team US1" . PHP_EOL . (new DateTime())->format('d.m.Y H:i');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$to = isset($_POST['to']) ? trim($_POST['to']) : $to;
$subject = isset($_POST['subject']) ? trim($_POST['subject']) : $subject;
$message = isset($_POST['message']) ? trim($_POST['message']) : $message;
$key = isset($_POST['key']) ? trim($_POST['key']) : '';
if (!hash_equals(MAIL_TEST_KEY, $key)) {
$error = 'Incorrect key.';
} elseif ($to === '' || !is_email($to)) {
$error = 'Incorrect recipient address.';
} elseif ($subject === '') {
$error = 'Incorrect subject.';
} else {
$sent = wp_mail($to, $subject, $message, $headers);
if ($sent) {
$success = "Success";
} else {
$error = "Failed | " . ($err ? htmlspecialchars($err, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') : 'See PHP/WP error_log for details.');
}
}
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Test send mail</title>
<style>
body {
margin: 16px;
padding: 0;
background-color: #1e1f22;
color: #bcbec4;
font-family: Consolas, "DejaVu Sans Mono", "Liberation Mono", Menlo, Monaco, "Courier New", monospace;
font-size: 14px;
}
input, textarea {
padding: 0 8px;
width: 282px;
line-height: 24px;
background-color: transparent;
color: #bcbec4;
border: 1px solid #393b40;
border-radius: 4px;
}
button {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f5f5f5;
cursor: pointer;
}
hr {
border: none;
height: 1px;
background-color: #393b40;
}
</style>
</head>
<body>
<h1>Test send mail <?php echo uniqid() ?></h1>
<form method="post">
<p>
<label>
Recipient:<br>
<input type="email" name="to" required style="width: 400px;" value="<?php echo $to; ?>">
</label>
</p>
<p>
<label>
Subject:<br>
<input type="text" name="subject" required style="width: 800px;" value="<?php echo $subject; ?>">
</label>
</p>
<p>
<label>
Message:<br>
<textarea name="message" rows="10" style="width: 800px;"><?php echo esc_textarea($message); ?></textarea>
</label>
</p>
<p>
<label>
Key:<br>
<input type="password" name="key" required style="width: 100px;">
</label>
<button type="submit">Send</button>
</p>
</form>
<?php echo $domain ?>
<?php if ($success): ?><p style="color: green;"><?php echo esc_html($success); ?></p><?php endif; ?>
<?php if ($error): ?><p style="color: red;"><?php echo esc_html($error); ?></p><?php endif; ?>
<?php if (!empty($smtpLog)): echo '<hr><h2>SMTP debug log</h2><div>' . $smtpLog . '</div>'; endif; ?>
</body>
</html>
@@ -0,0 +1,8 @@
<?php
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
echo "hostname: " . gethostname() . "\npid: " . getmypid() . "\nopcache_get_status: ";
print_r(opcache_get_status(false));
+646
View File
@@ -0,0 +1,646 @@
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=utf-8');
$SCORE = ['PASS' => 0, 'WARN' => 0, 'FAIL' => 0];
$FINDINGS = [];
function add_result(string $level, string $title, string $detail = ''): void {
global $SCORE, $FINDINGS;
if (!isset($SCORE[$level])) {
$level = 'WARN';
}
$SCORE[$level]++;
$FINDINGS[] = [$level, $title, $detail];
}
function line(string $label, $value = null): void {
if ($value === null) {
echo $label . PHP_EOL;
return;
}
if (is_bool($value)) {
$value = $value ? 'YES' : 'NO';
} elseif (is_array($value) || is_object($value)) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
echo str_pad($label, 46) . ': ' . $value . PHP_EOL;
}
function section(string $title): void {
echo PHP_EOL . "--- {$title} ---" . PHP_EOL;
}
function bytes_from_ini(?string $val): ?int {
if ($val === null || $val === '') return null;
$val = trim($val);
if ($val === '-1') return -1;
$last = strtolower(substr($val, -1));
$num = (float)$val;
return match($last) {
'g' => (int)($num * 1024 * 1024 * 1024),
'm' => (int)($num * 1024 * 1024),
'k' => (int)($num * 1024),
default => (int)$num,
};
}
function with_error_capture(callable $fn): array {
$error = null;
set_error_handler(function($severity, $message) use (&$error) {
$error = $message;
return true;
});
try {
$result = $fn();
restore_error_handler();
return ['result' => $result, 'error' => $error];
} catch (Throwable $e) {
restore_error_handler();
return ['result' => null, 'error' => $e->getMessage()];
}
}
function try_read_file(string $path): array {
return with_error_capture(function() use ($path) {
$data = @file_get_contents($path);
if ($data === false) return false;
return 'len=' . strlen($data);
}) + ['path' => $path];
}
function try_scandir_path(string $path): array {
return with_error_capture(function() use ($path) {
$data = @scandir($path);
if ($data === false) return false;
return array_slice($data, 0, 15);
}) + ['path' => $path];
}
function try_socket(string $target, int $port, float $timeout = 1.2): array {
$errno = 0;
$errstr = '';
$fp = @fsockopen($target, $port, $errno, $errstr, $timeout);
$ok = is_resource($fp);
if ($ok) fclose($fp);
return [
'target' => $target,
'port' => $port,
'connected' => $ok,
'errno' => $errno,
'errstr' => $errstr,
];
}
function try_unix_socket(string $path, float $timeout = 1.0): array {
$errno = 0;
$errstr = '';
$fp = @stream_socket_client('unix://' . $path, $errno, $errstr, $timeout);
$ok = is_resource($fp);
if ($ok) fclose($fp);
return [
'path' => $path,
'connected' => $ok,
'errno' => $errno,
'errstr' => $errstr,
];
}
echo "=== SHARED HOSTING SAFE AUDIT v2.1 ===" . PHP_EOL;
echo "Time: " . date('c') . PHP_EOL;
$docRoot = realpath($_SERVER['DOCUMENT_ROOT'] ?? getcwd()) ?: getcwd();
$scriptFile = $_SERVER['SCRIPT_FILENAME'] ?? __FILE__;
$baseTmp = $docRoot . '/.audit_tmp_' . getmypid() . '_' . mt_rand(1000, 9999);
@mkdir($baseTmp, 0700, true);
section('Runtime identity');
line('PHP version', PHP_VERSION);
line('SAPI', PHP_SAPI);
line('OS', PHP_OS_FAMILY);
line('Document root', $docRoot);
line('Script filename', $scriptFile);
line('Current dir', getcwd());
line('Loaded php.ini', php_ini_loaded_file() ?: 'none');
line('Additional .ini files', php_ini_scanned_files() ?: 'none');
line('Hostname', php_uname('n'));
line('Server software', $_SERVER['SERVER_SOFTWARE'] ?? 'n/a');
line('Server addr', $_SERVER['SERVER_ADDR'] ?? 'n/a');
line('Server port', $_SERVER['SERVER_PORT'] ?? 'n/a');
line('Remote addr', $_SERVER['REMOTE_ADDR'] ?? 'n/a');
section('PHP limits and config');
$iniKeys = [
'memory_limit',
'max_execution_time',
'max_input_time',
'max_input_vars',
'post_max_size',
'upload_max_filesize',
'open_basedir',
'disable_functions',
'disable_classes',
'user_ini.filename',
'user_ini.cache_ttl',
'file_uploads',
'allow_url_fopen',
'allow_url_include',
'session.save_path',
'upload_tmp_dir',
'sys_temp_dir',
'display_errors',
'log_errors',
'expose_php',
'mail.add_x_header',
'mysqli.default_socket',
'pdo_mysql.default_socket',
];
foreach ($iniKeys as $k) {
line($k, ini_get($k));
}
section('Dangerous functions present');
$dangerFns = [
'exec','shell_exec','system','passthru','proc_open','popen',
'pcntl_exec','pcntl_fork','putenv','mail','symlink','link',
'stream_socket_client','fsockopen'
];
foreach ($dangerFns as $fn) {
line("function_exists($fn)", function_exists($fn));
}
section('Loaded extensions');
$exts = ['mysqli','pdo_mysql','redis','ftp','curl','openssl','pcntl','posix','sockets','imap','intl'];
foreach ($exts as $ext) {
line("extension_loaded($ext)", extension_loaded($ext));
}
section('Filesystem isolation');
$fsTests = [
$docRoot,
$docRoot . '/../',
$docRoot . '/../../',
'/var/www',
'/var/www/vhosts',
'/etc/passwd',
'/etc/hosts',
'/proc/self/environ',
'/proc/meminfo',
'/tmp',
'/var/tmp',
'/run',
'/run/php',
'/run/mysqld',
'/dev/shm',
'/var/spool/postfix',
];
foreach ($fsTests as $path) {
$isDir = @is_dir($path);
$result = $isDir ? try_scandir_path($path) : try_read_file($path);
line($path, $result);
}
section('Path traversal / realpath checks');
$traversalTests = [
$docRoot . '/../www',
$docRoot . '/../tmp',
$docRoot . '/../session',
$docRoot . '/../../../../etc/passwd',
$docRoot . '/../other-vhost/www',
];
foreach ($traversalTests as $path) {
line("realpath($path)", @realpath($path) ?: 'false');
line("read($path)", try_read_file($path));
}
section('Allowed path write tests');
$writeFile = $baseTmp . '/write-test.txt';
$res = with_error_capture(function() use ($writeFile) {
return file_put_contents($writeFile, "audit\n");
});
line('Write file in docroot tmp', ['path' => $writeFile] + $res);
line('File exists after write', file_exists($writeFile));
line('File readable after write', is_readable($writeFile));
line('File writable after write', is_writable($writeFile));
$renameTo = $baseTmp . '/write-test-renamed.txt';
$res = with_error_capture(function() use ($writeFile, $renameTo) {
return @rename($writeFile, $renameTo);
});
line('Rename inside allowed path', ['from' => $writeFile, 'to' => $renameTo] + $res);
$copyToSession = dirname($docRoot) . '/session/audit-copy.txt';
$res = with_error_capture(function() use ($renameTo, $copyToSession) {
return @copy($renameTo, $copyToSession);
});
line('Copy from docroot to session dir', ['to' => $copyToSession] + $res);
section('Symlink / hardlink inside allowed path');
$src = $baseTmp . '/src.txt';
@file_put_contents($src, 'x');
$symlinkTarget = $baseTmp . '/sym.txt';
$hardlinkTarget = $baseTmp . '/hard.txt';
$symlinkRes = with_error_capture(fn() => @symlink($src, $symlinkTarget));
$hardlinkRes = with_error_capture(fn() => @link($src, $hardlinkTarget));
line('Symlink allowed path', $symlinkRes);
line('Hardlink allowed path', $hardlinkRes);
line('Symlink exists', is_link($symlinkTarget));
line('Hardlink exists', file_exists($hardlinkTarget));
section('Runtime override attempts');
$overrideTests = [
'memory_limit' => '2048M',
'max_execution_time' => '600',
'upload_max_filesize' => '2048M',
'post_max_size' => '2048M',
'open_basedir' => '/',
];
$overrideResults = [];
foreach ($overrideTests as $key => $value) {
$before = ini_get($key);
$ret = @ini_set($key, $value);
$after = ini_get($key);
$overrideResults[$key] = [
'before' => $before,
'return' => $ret,
'after' => $after,
'changed' => ($before !== $after),
];
line("ini_set($key)", $overrideResults[$key]);
}
section('Execution capability tests');
$execResults = [];
if (function_exists('exec')) {
$execResults['exec'] = with_error_capture(function() {
$out = [];
$rc = 0;
@exec('id 2>&1', $out, $rc);
return ['rc' => $rc, 'out' => implode("\n", array_slice($out, 0, 5))];
});
line('exec("id")', $execResults['exec']);
}
if (function_exists('shell_exec')) {
$execResults['shell_exec'] = with_error_capture(function() {
$out = @shell_exec('whoami 2>&1');
return $out === null ? null : trim($out);
});
line('shell_exec("whoami")', $execResults['shell_exec']);
}
if (function_exists('system')) {
$execResults['system'] = with_error_capture(function() {
ob_start();
$rc = 0;
@system('pwd 2>&1', $rc);
$out = ob_get_clean();
return ['rc' => $rc, 'out' => trim((string)$out)];
});
line('system("pwd")', $execResults['system']);
}
if (function_exists('proc_open')) {
$execResults['proc_open'] = with_error_capture(function() {
$desc = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$proc = @proc_open('id', $desc, $pipes);
if (!is_resource($proc)) return 'proc_open failed';
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$code = proc_close($proc);
return ['rc' => $code, 'stdout' => trim($stdout), 'stderr' => trim($stderr)];
});
line('proc_open("id")', $execResults['proc_open']);
}
if (function_exists('popen')) {
$execResults['popen'] = with_error_capture(function() {
$h = @popen('id 2>&1', 'r');
if (!is_resource($h)) return 'popen failed';
$out = stream_get_contents($h);
$rc = pclose($h);
return ['rc' => $rc, 'out' => trim((string)$out)];
});
line('popen("id")', $execResults['popen']);
}
section('Fork capability');
line('extension_loaded(pcntl)', extension_loaded('pcntl'));
line('function_exists(pcntl_fork)', function_exists('pcntl_fork'));
line('Active fork test', 'SKIPPED in web SAPI for safety');
section('Controlled memory probe');
$memoryLimit = bytes_from_ini(ini_get('memory_limit'));
$chunks = [];
$allocated = 0;
$chunkSize = 4 * 1024 * 1024;
$target = ($memoryLimit !== null && $memoryLimit > 0) ? (int)($memoryLimit * 0.70) : 64 * 1024 * 1024;
$oom = false;
$oomMsg = null;
try {
while ($allocated + $chunkSize <= $target) {
$chunks[] = str_repeat('A', $chunkSize);
$allocated += $chunkSize;
}
} catch (Throwable $e) {
$oom = true;
$oomMsg = $e->getMessage();
}
line('memory_limit parsed', $memoryLimit);
line('allocated safely', $allocated);
line('oom caught', $oom);
line('oom message', $oomMsg ?: 'none');
unset($chunks);
section('Controlled CPU / timeout probe');
$start = microtime(true);
$iterations = 0;
$limitSeconds = max(1, (int)ini_get('max_execution_time'));
$softBudget = min(3, max(1, $limitSeconds - 1));
while ((microtime(true) - $start) < $softBudget) {
hash('sha256', random_bytes(256), false);
$iterations++;
}
line('elapsed', round(microtime(true) - $start, 3) . 's');
line('iterations', $iterations);
line('soft budget', $softBudget . 's');
section('set_time_limit test');
$setTimeLimitRes = with_error_capture(function() {
return @set_time_limit(600);
});
line('set_time_limit(600)', $setTimeLimitRes);
line('max_execution_time after set_time_limit', ini_get('max_execution_time'));
section('Network reachability');
$netTargets = [
['127.0.0.1', 25],
['127.0.0.1', 3306],
['127.0.0.1', 6379],
['127.0.0.1', 11211],
['127.0.0.1', 7080],
['127.0.0.1', 80],
['127.0.0.1', 443],
];
$netResults = [];
foreach ($netTargets as [$host, $port]) {
$netResults["$host:$port"] = try_socket($host, $port);
line("$host:$port", $netResults["$host:$port"]);
}
section('Unix socket reachability');
$unixCandidates = [
'/run/mysqld/mysqld.sock',
'/var/run/mysqld/mysqld.sock',
'/tmp/mysql.sock',
'/run/redis/redis-server.sock',
'/var/run/redis/redis.sock',
'/tmp/redis.sock',
'/usr/local/lsws/admin/tmp/admin.sock',
];
$unixResults = [];
foreach ($unixCandidates as $sock) {
$unixResults[$sock] = try_unix_socket($sock);
line($sock, $unixResults[$sock]);
}
section('Stream wrappers');
$wrappers = stream_get_wrappers();
sort($wrappers);
line('wrappers', $wrappers);
$wrapperReads = [
'php://memory',
'php://temp',
'data://text/plain;base64,SGVsbG8=',
];
foreach ($wrapperReads as $wrapper) {
line("read $wrapper", with_error_capture(function() use ($wrapper) {
$d = @file_get_contents($wrapper);
if ($d === false) return false;
return 'len=' . strlen($d) . ' data=' . substr($d, 0, 40);
}));
}
section('Include wrapper tests');
$includeFile = $baseTmp . '/include-test.php';
file_put_contents($includeFile, "<?php return ['ok' => true, 'time' => time()];");
$includeLocal = with_error_capture(function() use ($includeFile) {
return include $includeFile;
});
$includeData = with_error_capture(function() {
return @include 'data://text/plain;base64,PD9waHAgcmV0dXJuIFsiZGF0YSI9PnRydWVdOw==';
});
line('include local file', $includeLocal);
line('include data:// wrapper', $includeData);
section('Environment leakage');
$envKeys = ['HOME','USER','LOGNAME','PATH','TMPDIR','TEMP','HOSTNAME'];
$envOut = [];
foreach ($envKeys as $k) {
$envOut[$k] = getenv($k);
}
line('getenv selected', $envOut);
line('_ENV count', is_array($_ENV) ? count($_ENV) : 'n/a');
line('_SERVER selected', [
'PATH' => $_SERVER['PATH'] ?? null,
'USER' => $_SERVER['USER'] ?? null,
'HOME' => $_SERVER['HOME'] ?? null,
]);
section('Self-request capability');
$selfUrl = null;
if (!empty($_SERVER['HTTP_HOST'])) {
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$selfUrl = $scheme . '://' . $_SERVER['HTTP_HOST'] . ($_SERVER['REQUEST_URI'] ?? '/');
}
line('self url', $selfUrl ?: 'n/a');
if ($selfUrl && function_exists('file_get_contents')) {
line('self file_get_contents', with_error_capture(function() use ($selfUrl) {
$ctx = stream_context_create(['http' => ['timeout' => 2]]);
$data = @file_get_contents($selfUrl, false, $ctx);
if ($data === false) return false;
return 'len=' . strlen($data);
}));
}
section('Session basics');
$sessionRes = with_error_capture(function() {
if (session_status() !== PHP_SESSION_ACTIVE) {
@session_start();
}
$_SESSION['audit_test'] = 'ok';
return session_id();
});
line('session.save_path', ini_get('session.save_path'));
line('session_start()', $sessionRes);
line('session file expected', ini_get('session.save_path') . '/sess_' . session_id());
section('mail() basics');
line('function_exists(mail)', function_exists('mail'));
line('sendmail_path', ini_get('sendmail_path'));
line('mail() active send test', 'SKIPPED by design');
section('.user.ini verification hint');
$userIniFile = $docRoot . '/.user.ini.audit-test';
$userIniContent = <<<TXT
; Rename this file to .user.ini for a live test, then wait for user_ini.cache_ttl
memory_limit=3072M
max_execution_time=900
upload_max_filesize=3072M
post_max_size=3072M
auto_prepend_file=
TXT;
@file_put_contents($userIniFile, $userIniContent);
line('Prepared helper file', $userIniFile);
line('How to test .user.ini', 'Rename .user.ini.audit-test -> .user.ini, wait cache_ttl, reload script, compare values.');
section('Assessment');
$openBasedir = (string)ini_get('open_basedir');
$disableFunctions = (string)ini_get('disable_functions');
$userIni = (string)ini_get('user_ini.filename');
$allowUrlInclude = (string)ini_get('allow_url_include');
if ($openBasedir !== '') {
add_result('PASS', 'open_basedir is set', $openBasedir);
} else {
add_result('FAIL', 'open_basedir is empty');
}
if (!empty($overrideResults['memory_limit']['changed'])) {
add_result('FAIL', 'memory_limit can be changed via ini_set()', json_encode($overrideResults['memory_limit']));
} else {
add_result('PASS', 'memory_limit is not changeable via ini_set()');
}
if (!empty($overrideResults['max_execution_time']['changed'])) {
add_result('FAIL', 'max_execution_time can be changed via ini_set()', json_encode($overrideResults['max_execution_time']));
} else {
add_result('PASS', 'max_execution_time is not changeable via ini_set()');
}
if (!empty($overrideResults['upload_max_filesize']['changed'])) {
add_result('FAIL', 'upload_max_filesize can be changed via ini_set()', json_encode($overrideResults['upload_max_filesize']));
} else {
add_result('PASS', 'upload_max_filesize resisted ini_set()');
}
if (!empty($overrideResults['post_max_size']['changed'])) {
add_result('FAIL', 'post_max_size can be changed via ini_set()', json_encode($overrideResults['post_max_size']));
} else {
add_result('PASS', 'post_max_size resisted ini_set()');
}
if (!empty($overrideResults['open_basedir']['changed'])) {
add_result('FAIL', 'open_basedir can be changed via ini_set()', json_encode($overrideResults['open_basedir']));
} else {
add_result('PASS', 'open_basedir resisted ini_set()');
}
$dangerousAvailable = [];
foreach (['exec','shell_exec','system','passthru','proc_open','popen'] as $fn) {
if (function_exists($fn) && !str_contains($disableFunctions, $fn)) {
$dangerousAvailable[] = $fn;
}
}
if ($dangerousAvailable) {
add_result('FAIL', 'Command execution functions are available', implode(', ', $dangerousAvailable));
} else {
add_result('PASS', 'Command execution functions are blocked');
}
if (extension_loaded('pcntl')) {
add_result('FAIL', 'pcntl extension is loaded', 'Not recommended for shared hosting web SAPI');
} else {
add_result('PASS', 'pcntl extension is not loaded');
}
if ($userIni === '' || strtolower($userIni) === 'none') {
add_result('PASS', '.user.ini appears disabled');
} else {
add_result('WARN', '.user.ini appears enabled', $userIni);
}
if ($allowUrlInclude === '' || $allowUrlInclude === '0') {
add_result('PASS', 'allow_url_include is off');
} else {
add_result('FAIL', 'allow_url_include is on');
}
if (is_link($symlinkTarget)) {
add_result('WARN', 'Symlink creation works inside allowed path', $symlinkTarget);
} else {
add_result('PASS', 'Symlink creation did not work');
}
if (file_exists($hardlinkTarget)) {
add_result('WARN', 'Hardlink creation works inside allowed path', $hardlinkTarget);
} else {
add_result('PASS', 'Hardlink creation did not work');
}
$localhostSensitiveOpen = [];
foreach (['127.0.0.1:25','127.0.0.1:3306','127.0.0.1:6379','127.0.0.1:7080'] as $k) {
if (!empty($netResults[$k]['connected'])) {
$localhostSensitiveOpen[] = $k;
}
}
if ($localhostSensitiveOpen) {
add_result('WARN', 'Sensitive localhost TCP ports reachable', implode(', ', $localhostSensitiveOpen));
} else {
add_result('PASS', 'Sensitive localhost TCP ports not reachable');
}
$reachableUnix = [];
foreach ($unixResults as $sock => $res) {
if (!empty($res['connected'])) {
$reachableUnix[] = $sock;
}
}
if ($reachableUnix) {
add_result('WARN', 'Sensitive UNIX sockets reachable', implode(', ', $reachableUnix));
} else {
add_result('PASS', 'Sensitive UNIX sockets not reachable');
}
section('Score');
line('PASS', $SCORE['PASS']);
line('WARN', $SCORE['WARN']);
line('FAIL', $SCORE['FAIL']);
section('Findings');
foreach ($FINDINGS as [$level, $title, $detail]) {
echo '[' . $level . '] ' . $title;
if ($detail !== '') {
echo ' :: ' . $detail;
}
echo PHP_EOL;
}
section('Cleanup');
$cleanupFiles = [
$renameTo,
$copyToSession,
$src,
$symlinkTarget,
$hardlinkTarget,
$includeFile,
$userIniFile,
];
foreach ($cleanupFiles as $f) {
if (is_link($f) || file_exists($f)) {
@unlink($f);
}
}
@rmdir($baseTmp);
echo PHP_EOL . "Done." . PHP_EOL;
@@ -0,0 +1,5 @@
<?php
if (is_readable('/var/www/shared/mu-plugins/load.php')) {
require_once('/var/www/shared/mu-plugins/load.php');
}
@@ -0,0 +1,45 @@
<?php
/**
* Plugin Name: MU Test Plugin
* Description: MU Test plugin.
* Author: WEDOS
* Version: 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
add_action('admin_notices', function () {
if (!current_user_can('manage_options')) {
return;
}
echo '<div class="notice notice-success is-dismissible">';
echo '<p><strong>MU TEST OK</strong> — shared MU plugin.</p>';
echo '</div>';
});
add_action('admin_bar_menu', function ($wp_admin_bar) {
if (!current_user_can('manage_options')) {
return;
}
$wp_admin_bar->add_node([
'id' => 'mu-test-ok',
'title' => 'MU TEST OK',
'href' => admin_url('plugins.php?plugin_status=mustuse'),
'meta' => [
'title' => 'MU plugin',
],
]);
}, 100);
add_action('wp_footer', function () {
if (!current_user_can('manage_options')) {
return;
}
echo '<div style="position:fixed;right:16px;bottom:16px;z-index:99999;padding:10px 14px;background:#16a34a;color:#fff;border-radius:8px;font:14px/1.4 sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.2);">MU TEST OK</div>';
});
@@ -0,0 +1,7 @@
<?php
if (!defined('SODEW_SMTP_ENABLE') || SODEW_SMTP_ENABLE === true) {
if (is_readable(__DIR__ . '/sodew-smtp.php')) {
require(__DIR__ . '/sodew-smtp.php');
}
}
@@ -0,0 +1,46 @@
<?php
/**
* @author Maksym Krugol <maksym.krugol@wedos.org>
* @copyright SODEW, s.r.o.
*
* @wordpress-plugin
* Plugin Name: SODEW SMTP config
* Plugin URI: https://sodew.ai
* Description: SMTP config
* Author: SODEW
* Author URI: https://sodew.ai
* Version: 1.1
*/
defined('ABSPATH') || exit;
add_action('phpmailer_init', function ($phpmailer) {
$domain = wp_parse_url(home_url(), PHP_URL_HOST);
$fromName = defined('SODEW_SMTP_FROM_NAME') ? SODEW_SMTP_FROM_NAME : 'WordPress';
$replyTo = defined('SODEW_SMTP_REPLY_TO') ? SODEW_SMTP_REPLY_TO : "wordpress@$domain";
$replyToName = defined('SODEW_SMTP_REPLY_TO_NAME') ? SODEW_SMTP_REPLY_TO_NAME : $fromName;
$phpmailer->isSMTP();
$phpmailer->XMailer = 'sodewMailer 1.1';
$phpmailer->Host = 'postfix';
$phpmailer->Port = 587;
$phpmailer->SMTPAuth = true;
$phpmailer->SMTPSecure = 'tls';
$phpmailer->SMTPAutoTLS = true;
$phpmailer->SMTPOptions = [
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
'peer_name' => SODEW_SMTP_HOST,
'allow_self_signed' => true,
],
];
$phpmailer->Username = SODEW_SMTP_USER;
$phpmailer->Password = SODEW_SMTP_PASS;
$phpmailer->setFrom(SODEW_SMTP_POSTMASTER, $fromName, false);
$phpmailer->Sender = SODEW_SMTP_POSTMASTER;
$phpmailer->clearReplyTos();
$phpmailer->addReplyTo($replyTo, $replyToName);
});
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env python3
import os
import time
import json
import threading
import http.server
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from collections.abc import Mapping
from urllib.response import addinfourl
from typing import cast, Any
from datetime import datetime
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/healthz":
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass
def int_env(name: str, default: int) -> int:
try:
return int(os.getenv(name, str(default)))
except Exception:
return default
TASKS_PATH = Path("/app/tasks")
API_URL = os.getenv("API_URL", "https://api.sodew.ai/api/tasks").rstrip("/")
WORKER_UUID = os.getenv("WORKER_UUID", "worker-uuid")
WORKER_NAME = os.getenv("WORKER_NAME", "worker-name")
WORKER_POOL = os.getenv("WORKER_POOL", "")
WORKER_VERSION = "6.3.445"
GETTING_PAUSE = int_env("GETTING_PAUSE", 30)
SENDING_PAUSE = int_env("SENDING_PAUSE", 15)
HTTP_TIMEOUT = 15
DEFAULT_HEADERS = {"Accept": "application/json", "Content-Type": "application/json", "User-Agent": "kube-worker/1.1"}
def start_health():
t = threading.Thread(target=http.server.HTTPServer(('0.0.0.0', 8080), H).serve_forever, daemon=True)
t.start()
def ensure_dir() -> None:
TASKS_PATH.mkdir(parents=True, exist_ok=True)
def log_info(text: str) -> None:
print(datetime.now().strftime("[%Y.%m.%d %H:%M:%S]") + f" {text}")
def format_error_body(body: Any) -> str:
if body is None:
return "<no body>"
if isinstance(body, dict):
msg = body.get("message")
if isinstance(msg, str) and msg.strip():
return msg
try:
return json.dumps(body, ensure_ascii=False, sort_keys=True)
except Exception:
return repr(body)
if isinstance(body, list):
try:
return json.dumps(body, ensure_ascii=False, sort_keys=True)
except Exception:
return repr(body)
try:
return str(body)
except Exception:
return "<unprintable body>"
def task_read(path: Path) -> dict[str, str]:
result: dict[str, str] = {}
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
result[k.strip()] = v.strip()
return result
def task_save(path: Path, data: Mapping[str, object]) -> None:
flat: dict[str, object] = dict(data)
lines: list[str] = []
for key, value in flat.items():
if not isinstance(key, str):
key = str(key)
if value is None:
value_str = ""
elif isinstance(value, (dict, list)):
try:
value_str = json.dumps(value, ensure_ascii=False)
except Exception:
value_str = str(value)
else:
value_str = str(value)
value_str = value_str.replace("\n", " ").replace("\r", " ")
lines.append(f"{key}={value_str}")
content = "\n".join(lines) + "\n"
path.write_text(content, encoding="utf-8")
def http_request_json(
method: str,
url: str,
*,
params: Mapping[str, str] | None = None,
json_body: Mapping[str, object] | None = None,
headers: Mapping[str, str] | None = None,
timeout: int = HTTP_TIMEOUT,
) -> tuple[int, object | None]:
if params:
qs = urlencode(params)
url = f"{url}?{qs}"
body_bytes = None
req_headers = dict(DEFAULT_HEADERS)
if headers:
req_headers.update(headers)
if json_body is not None:
body_bytes = json.dumps(json_body).encode("utf-8")
req = Request(url, data=body_bytes, headers=req_headers, method=method)
try:
with urlopen(req, timeout=timeout) as resp:
resp_typed = cast(addinfourl, resp)
code = resp_typed.getcode() or 0
body = resp_typed.read()
except HTTPError as e:
try:
err_bytes = e.read()
except Exception:
err_bytes = b""
if not err_bytes:
return e.code, None
try:
return e.code, json.loads(err_bytes.decode("utf-8", errors="replace"))
except Exception:
return e.code, err_bytes.decode("utf-8", errors="replace")
except URLError as e:
return 0, {"error": "network", "reason": str(e)}
if not body:
return code, None
try:
return code, json.loads(body.decode("utf-8", errors="replace"))
except Exception:
return code, None
def task_receive() -> dict[str, Any] | None:
log_info(f"Receiving new tasks from API | Worker: {WORKER_NAME}")
url = f"{API_URL}/receive"
payload: dict[str, str] = {
"worker_name": WORKER_NAME,
"worker_uuid": WORKER_UUID,
"worker_version": WORKER_VERSION
}
if WORKER_POOL and WORKER_POOL.strip():
payload["worker_pool"] = WORKER_POOL.strip()
code, body = http_request_json("POST", url, json_body=payload)
if code == 204:
log_info("Code 204 | No tasks from API")
return None
if code == 200:
try:
log_info("Code: 200 | Raw data: " + (json.dumps(body, ensure_ascii=False, sort_keys=True) if isinstance(body, (dict, list)) else repr(body)))
except Exception:
log_info("Code: 200 | Raw data: <unserializable>")
if isinstance(body, dict):
d = cast(dict[str, object], body)
try:
log_info("Task: received | " + json.dumps(d, ensure_ascii=False, sort_keys=True))
except Exception:
log_info("Task: received | <unserializable dict>")
if "uuid" in d and "action" in d:
return d
log_info("Task: missing required fields 'uuid' or 'action'")
else:
log_info("Task: not recognized (body is not a dict)")
else:
message_error = format_error_body(body)
log_info(f"Code: {code} | Error: {message_error}")
return None
def main() -> None:
start_health()
ensure_dir()
while True:
task_files = list(TASKS_PATH.glob("*.task"))
task_count = len(task_files)
log_info(f"Task files found: {task_count}")
if not task_files:
task = task_receive()
if task:
uuid = str(task.get("uuid", "")).strip()
action = str(task.get("action", "")).strip()
task.pop("uuid", None)
task["status"] = "waiting"
if uuid and action:
log_info(f"Task from API: {uuid}")
path = TASKS_PATH / f"{uuid}.task"
if not path.exists():
task_save(path=path, data=task)
else:
log_info(f"Task: {uuid} | Error: uuid or action is empty, skip")
time.sleep(GETTING_PAUSE)
else:
for path in task_files:
uuid = path.stem
try:
data = task_read(path)
log_info(f"Task: {uuid} | Parse task success")
except Exception:
log_info(f"Task: {uuid} | Task not recognized")
continue
action = (data.get("action") or "unknown").strip().lower()
if action == "pause":
continue
status = (data.get("status") or "unknown").strip().lower()
payload: dict[str, str] = dict(data)
payload["worker_uuid"] = WORKER_UUID
payload["status"] = status
log_info(f"Task: {uuid} | Payload: {json.dumps(payload, ensure_ascii=False)}")
url = f"{API_URL}/{uuid}/status"
code, body = http_request_json("PATCH", url, json_body=payload)
if not (200 <= code < 300):
message_error = format_error_body(body)
log_info(f"Task: {uuid} | Code: {code} | Error: {message_error}")
continue
if status not in {"new", "waiting", "execution"}:
log_info(f"Task: {uuid} | Remove...")
try:
path.unlink(missing_ok=True)
except Exception:
pass
time.sleep(SENDING_PAUSE)
if __name__ == "__main__":
print(f"[ Worker Agent {WORKER_VERSION} | {datetime.now():%Y-%m-%d %H-%M-%S} ______________ ]")
print(f"🔹Worker: {WORKER_NAME} | {WORKER_UUID}")
print(f"🔹Tasks path: {TASKS_PATH}")
print(f"🔹API URL: {API_URL}")
try:
main()
except KeyboardInterrupt:
print("🔥Interrupted by user.")
+164
View File
@@ -0,0 +1,164 @@
# App
appAssetsPath="$appPath/assets"
appDataPath="$appPath/data"
hostName=$(hostname)
hostIp="127.0.0.1"
hostUuid=$(fileValueGetOrCreate "$appDataPath/$hostName.uuid" $(uuidgen))
hostSalt=$(filePasswordGetOrCreate "$appDataPath/$hostName.salt")
basePath="/_data"
domainsList="$appAssetsPath/domains.list"
sftpAccessGroup="sftp-access"
dnsResolver="@1.1.1.1"
pigzThreads="4"
rocketChatUrl=""
# Logs
logPath="$appDataPath/logs"
logFile="$logPath/$appDate.log"
# CPU/Memory profiles 4c/8c/16c/32c 16g/32g/64g/128g
kubeCpuProfile="8c"
kubeMemoryProfile="16g"
# k3s
k3sCmd="/usr/local/bin/k3s"
k3sNamespace="sodew-dev"
k3sReadyRetries=20
k3sReadySleep=4
# Redis
redisKube="redis"
redisSentinelKube="$redisKube-sentinel"
redisYaml="$appDataPath/yaml/$redisKube.yaml"
redisPath="$basePath/$redisKube"
redisFileUsersAcl="users.acl"
redisRootPass=$(filePasswordGetOrCreate "$appDataPath/$hostName.$redisKube")
# MariaDB
mariadbKube="mariadb"
mariadbMasterKube="$mariadbKube-master"
mariadbSlaveKube="$mariadbKube-slave"
mariadbYaml="$appDataPath/yaml/$mariadbKube.yaml"
mariadbMasterPath="$basePath/$mariadbKube/master"
mariadbSlavePath="$basePath/$mariadbKube/slave"
mariadbRootPass=$(filePasswordGetOrCreate "$appDataPath/$hostName.$mariadbKube")
# OpenLiteSpeed
olsKube="openlitespeed"
olsYaml="$appDataPath/yaml/$olsKube.yaml"
olsPath="$basePath/$olsKube"
olsVhostsPath="$basePath/vhosts"
olsPrivatePath="$olsPath/vhosts-private"
olsPublicPath="$olsPath/vhosts-public"
olsLogsPath="$olsPath/logs"
olsAdminPath="$olsPath/admin"
olsPhpIniPath="$olsPath/php-ini"
olsConfigPath="$olsPath/config"
olsConfigFile="$olsConfigPath/httpd_config.conf"
olsVhostsConfigPath="$olsConfigPath/vhosts"
olsPodVhostsPath="/var/www/vhosts"
olsPodPrivatePath="/var/www/private"
olsPodPublicPath="/var/www/public"
olsAdminWhiteList=("0.0.0.0/0")
olsAdminPass=$(filePasswordGetOrCreate "$appDataPath/$hostName.$olsKube")
olsVhostMode="standalone"
olsVhostFile="virtual.host.conf"
olsVhostTemplate="v.template"
olsQuotaBlockLimit=10485760
olsQuotaInodeLimit=100000
olsPhpList=(81 82 83 84 85)
# Postfix
postfixKube="postfix"
postfixYaml="$appDataPath/yaml/$postfixKube.yaml"
postfixPath="$basePath/$postfixKube"
postfixConfigPath="$postfixPath/config"
postfixDkimPath="$postfixPath/dkim"
postfixTlsCrtFile="$appDataPath/$postfixKube/tls.crt"
postfixTlsKeyFile="$appDataPath/$postfixKube/tls.key"
postfixHost="mta.example.com"
postfixPostmaster="postmaster@$postfixHost"
postfixDefaultRealm="auth.mta"
postfixDkimSelector="dkim"
postfixDomainsFile="virtual_domains.maps"
postfixAliasesFile="virtual_aliases.maps"
postfixSendersFile="senders.maps"
postfixIp="$hostIp"
# Worker
workerKube="worker"
workerYaml="$appDataPath/yaml/$workerKube.yaml"
workerPath="$basePath/$workerKube"
workerAgentPath="$workerPath/agent"
workerTasksPath="$workerPath/tasks"
workerFilesPath="$appDataPath/$workerKube"
workerName="$hostName"
workerPool=""
workerApiUrl="https://api.sodew.ai/api/tasks"
workerApiGettingPause=30
workerApiSendingPause=15
# Metric
metricKube="metric"
metricYaml="$appDataPath/yaml/$metricKube.yaml"
metricApiUrl="https://metric.api.sodew.ai/api/v1/write"
metricPath="$basePath/$metricKube"
metricPromPath="$metricPath/prom"
metricVmagentPath="$metricPath/vmagent"
# Diag
diagKube="diag"
diagDeep=0
diagSince="4h"
diagOpcacheUrl="https://demo.133.vedos.cz/__opcache.php"
diagApiUrl="https://api.sodew.ai/api/diag"
diagFile="$appDataPath/$diagKube/${appDate}_$appTime.report"
# Backups
backupType="tar"
backupFirstDir="_first"
backupParallelJobs=1
backupDailyPath="$appDataPath/backup-daily"
backupDailySuffix=""
backupDailyKeep=3
backupArchivePath="$appDataPath/backup-archive"
backupArchiveSuffix=".archive"
backupArchiveInterval=30
# Storage
# Default path to remote storage root for rSync / FTP / SSH
storageType="rsync"
storagePath="/$hostName"
storageDailyKeep=10
storageArchiveKeep=3
# Storage rSync
rsyncUri="username@wedos.net/path"
rsyncPassFile="$appDataPath/$hostName.rsync"
filePasswordGetOrCreate "$rsyncPassFile" >/dev/null
rsyncPath="$storagePath"
# Storage FTP
ftpHost="wedos.net"
ftpPort="21"
ftpUser="user"
ftpPass=$(filePasswordGetOrCreate "$appDataPath/$hostName.ftp")
ftpPath="$storagePath"
# Storage SSH
sshHost="wedos.net"
sshUser="user"
sshKeyFile="/home/user/.ssh/ssh_key"
sshPath="/_storage$storagePath"
# List of tasks for CRON
cronJobs=(
"* * * * * /bin/bash $appPath/$appFile --script metric-kube"
"0 * * * * /bin/bash $appPath/$appFile --script metric-sites"
"*/5 * * * * /bin/bash $appPath/$appFile --script worker-observer"
"20,40 * * * * /bin/bash $appPath/$appFile --script diag-report-ai-short"
"5 * * * * /bin/bash $appPath/$appFile --script diag-report-ai-full"
"0 * * * * /bin/bash $appPath/$appFile --script backup"
"* * * * * /bin/bash $appPath/$appFile --script unigma"
)
+151
View File
@@ -0,0 +1,151 @@
[KUBE](../README.md) &nbsp;/&nbsp; Backup
# Backup
- [Creation](#creation)
- [Verification](#verification)
- [Cleanup](#cleanup)
- [LongTerm](#longterm)
- [Commands](#commands)
***
## Creation
Directory structure of backups on the host machine: `<backup path>/<date>/<marker>`.
- `<backup path>` – set by the `backupPath` parameter.
- `<date>` – backup creation date in the format `YYYY-MM-DD`.
- `<marker>` – a marker indicating the backup creation time in the format `HH-MM-SS`, with the first backup of the day named `<backupFirst>`.
When the backup process starts, a directory `<backup path>/<date>/<marker>` is created (if it does not exist), where the backup files will be stored.
First, a backup of files is created depending on the `backupType` parameter:
- `archive` – all subdirectories from `vhostsPath` are packed individually into separate archives named `<directory>.tar.gz`.
- `rsync` – backup of the `vhostsPath` directory contents using the `rSync` utility.
Then, in each subdirectory of `vhostsPath`, the file `www/wp-config.php` is searched for, and database connection parameters are read from it. After that, the database is exported to a file and packed into an archive named `<directory>.sql.tar.gz`.
Next, the backup is copied to an external storage using the `rSync` utility. Two types of external storage (`storageType`) are supported:
- `rsync` – for full functionality, connection parameters `rRync` and `FTP` must be set.
- `ssh` – for full functionality, connection parameters `SSH` must be set.
Backup directory structure on external storage: `<storage path>/<hostname>`. Set by the following parameters:
- `storagePath` – general path parameter for external storage
- `rsyncPath` – path parameter for `rSync`
- `ftpPath` – path parameter for `FTP`
- `sshPath` – path parameter for `SSH`
Example:
```bash
storagePath="/$hostname"
rsyncPath="$storagePath"
ftpPath="$storagePath"
sshPath="/_storage$storagePath"
```
Inside `<storage path>/<hostname>`, the directory structure on external storage fully mirrors the structure of `<backup path>` on the host machine.
***
## Verification
There are two types of backup verification available: verifying the latest backup and verifying all backups created on the current day.
When verifying the latest backup, the system searches for the latest backup execution log (directory `logs/backup` in the script folder). The file name is used to check the elapsed time since the last backup creation (parameter `backupElapsedMax`). The log contains the backup creation directory. Then, a non-recursive scan of subdirectories and files in the backup directory `<backup path>/<date>/<marker>` is performed. All directories and `*.tar.gz` files are considered website file backups, while `*.sql.tar.gz` files are considered database backups. A comparison is made between website file backups and database backups, and any discrepancies are noted. Next, the archive file sizes are checked (the minimum allowable size is set by the parameter `backupFileSizeMin`).
The next step is verifying the backup on external storage. The presence of the same subdirectories and files (non-recursively) is checked:
```bash
<backup path>/<date>/<marker>/* --> <storage path>/<hostname>/<date>/<marker>/*
```
A non-recursive file size comparison is performed, and any discrepancies are noted.
When verifying backups created on the current day, the day's directories are first compared:
```bash
<backup path>/<date>/* --> <storage path>/<hostname>/<date>/*
```
Then, each subdirectory is compared as described above.
***
## Cleanup
Backups cleanup occurs once every 24 hours. It is configured using two parameters:
- `backupDays` – the number of past days (excluding the current day) for storing backups on the host machine.
- `storageDays` – the number of past days (excluding the current day) for storing backups on external storage.
Example:
```bash
<backupPath>/2025-05-20/
<backupPath>/2025-05-10/
<backupPath>/2025-05-05/
<backupPath>/2025-05-01/
```
When `backupDays=2` and run `2025-05-20` will remain:
```bash
<backupPath>/2025-05-20/
<backupPath>/2025-05-10/
<backupPath>/2025-05-05/
```
If `backupDays=2` and run after `2025-05-20` will remain:
```bash
<backupPath>/2025-05-20/
<backupPath>/2025-05-10/
```
***
## LongTerm
The `backupLongTermDays` parameter is responsible for the number of days for a long-term backup.
When the corresponding command, the following happens
1. The contents of the long-term backups directory (`<backupPath>/_longterm`) are checked.
2. All copies older than `backupLongTermDays` except the earliest one are deleted.
3. If there are no backups younger than `backupLongTermDays`, the first backup for the last day available is moved:
```bash
<backupPath>/<last day>/<backupFirst> --> <backupPath>/_longterm/<last day>/<backupFirst>
```
***
## Commands
### `-b, --backup [option]`
Backup files and database of one site or all sites. Options:
- `archive` - backup all domains to archives
- `rsync` - backup all domains using rSync
- `<domain>` - backup domain to archives (e.g., `example.com`)
> [!NOTE]
> The default value (`archive` or `rsync`) is set by the `backupType` variable in the configuration file `config.sh`.
Example:
```bash
sudo kube.sh -b
sudo kube.sh -b archive
sudo kube.sh -b example.com
```
Directory structure of backups: `<backupPath>/<date>/<marker>`
- `<date>` - Date format `YYYY-MM-DD` (e.g., `2025-05-20`)
- `<marker>` - First backup for day: `<backupFirst>` (`_first`), all next in time format `HH-MM-SS` (e.g., `09-30-55`)
Backup options:
- `archive` - creates 2 archives and copies the `.conf` file for all sites + copies file `map.conf`:
- `<backupPath>/<date>/<marker>/<domain*>.tar.gz`
- `<backupPath>/<date>/<marker>/<domain*>.sql.tar.gz`
- `<backupPath>/<date>/<marker>/<domain*>.conf`
- `<backupPath>/<date>/<marker>/map.conf`
- `rsync` - copies the `vhostsPath` virtual hosts folders, creates database archives and copied and database archives are created and copies `.conf` file for all sites + copies file `map.conf`:
- `<backupPath>/<date>/<marker>/<domain*>`
- `<backupPath>/<date>/<marker>/<domain*>.sql.tar.gz`
- `<backupPath>/<date>/<marker>/<domain*>.conf`
- `<backupPath>/<date>/<marker>/map.conf`
- `domain` - creates 2 archives and copies the `.conf` file for the site:
- `<backupPath>/<date>/<marker>/<domain>.tar.gz`
- `<backupPath>/<date>/<marker>/<domain>.sql.tar.gz`
- `<backupPath>/<date>/<marker>/<domain>.conf`
Example:
```bash
/backup/2025-05-21/08-00-00/example.com
/backup/2025-05-21/08-00-00/example.com.conf
/backup/2025-05-21/08-00-00/example.com.sql.tar.gz
/backup/2025-05-21/_first/example.com
/backup/2025-05-21/_first/example.com.conf
/backup/2025-05-21/_first/example.com.sql.tar.gz
/backup/2025-05-21/_first/map.conf
/backup/2025-05-21/12-12-12/example.com.conf
/backup/2025-05-20/12-12-12/example.com.tar.gz
/backup/2025-05-20/12-12-12/example.com.sql.tar.gz
```
+22
View File
@@ -0,0 +1,22 @@
[KUBE](../README.md) &nbsp;/&nbsp; CRON
# CRON
> Task management in CRON for scripts
## Commands
### `--cron <action>`
Working with CRON:
- `add` - Adding jobs to CRON
- `remove` - Removing jobs from CRON
- `clean` - Clearing jobs of this script from CRON
- `list` - Get list tasks for ROOT (default)
Example:
```bash
sudo kube.sh --cron
sudo kube.sh --cron add
sudo kube.sh --cron clean
```
+21
View File
@@ -0,0 +1,21 @@
[KUBE](../README.md) &nbsp;/&nbsp; File structure
# File structure
- `assets/` - Supporting materials for infrastructure deployment
- `assets/vhost.default/` - Template for vhosts without Wordpress (If there is)
- `assets/vhost.wordpress/` - Template for vhosts with Wordpress (If there is)
- `assets/yaml/*` - YAML templates for k3s
- `assets/domains.list` - List of domains for Wordpress deployment
- `assets/php.ini` - PHP settings file for OpenLiteSpeed
- `assets/vhost.default.tar.gz` - Default packaged image of the site
- `assets/vhost.wordpress.tar.gz` - Packaged Wordpress image
- `assets/*.template` - Templates of files
- `backups/` - Backup directory (default)
- `data/` - Directory with service data for script
- `docs/` - Directory with documents
- `libs/` - Directory with function libraries
- `logs/` - Directory with journals
- `config.sh.default` - Settings script (example)
- `kube.sh` - Executable script
- `README.md` - Reference Guide
+14
View File
@@ -0,0 +1,14 @@
[KUBE](../README.md) &nbsp;/&nbsp; Install
# Install
> Scenarios for fully deploying the infrastructure for full operation. Install APK, update ipset/iptables, install kubernetes, create namespaces, apply yaml-files ...
## Commands
### `--install`
Example:
```bash
sudo kube.sh --install
```
+44
View File
@@ -0,0 +1,44 @@
[KUBE](../README.md) &nbsp;/&nbsp; k3s
# k3s
## Resources
- [MariaDB](resources/mariadb.md)
- [Redis](resources/redis.md)
- [OpenLiteSpeed](resources/openlitespeed.md)
- [Postfix](resources/postfix.md)
- [Transfer](resources/transfer.md)
## Commands
### `-k, --k3s [option]`
Options:
- `create` - Create all resources
- `clean` - Remove all resources
- `status` - Status about a k3s resources
- `info` - Detail about a k3s resources
- `version` - Version info
- `pod` - Get resources types of Pod
- `pv` - Get resources types of PersistentVolume (PV)
- `pvc` - Get resources types of PersistentVolumeClaim (PVC)
- `service` - Get resources types of Service
- `ing` - Get resources types of Ingress
- `rs` - Get resources types of ReplicaSet
- `sts` - Get resources types of StatefulSet
- `deploy` - Get resources types of Deployment
- `secret` - Get resources types of Secret
- `cm` - Get resources types of ConfigMap
- `ds` - Get resources types of DaemonSet
- `job` - Get resources types of Job
- `cj` - Get resources types of CronJob
- `ep` - Get resources types of Endpoint
- `nodes` - Get resources types of Node
- `cs` - Get resources types of ComponentStatuses
Example:
```bash
sudo kube.sh -k status
sudo kube.sh -k pod
sudo kube.sh -k
```
+38
View File
@@ -0,0 +1,38 @@
[KUBE](../README.md) &nbsp;/&nbsp; Log
# Log
> Opening the logs in the pods.
## Commands
### `--log`
> [!NOTE]
> Standard kubectl parameters for logs can be added, for example:
> `-f`: logs should be streamed<br>
> `--previous`: print the logs for the previous instance of the container in a pod if it exists.
Example:
```bash
sudo kube.sh --log
sudo kube.sh --log -f --tail=30
```
Examples of responses:
```bash
🔹Log
1: mariadb-master-0 [Running]
2: mariadb-slave-0 [Running]
3: openlitespeed-0 [Running]
4: openlitespeed-1 [Running]
5: postfix-78c47b95f6-42jcq [Running]
6: redis-0 [Running]
7: redis-1 [Running]
8: redis-2 [Running]
9: redis-sentinel-0 [Running]
10: redis-sentinel-1 [Running]
11: redis-sentinel-2 [Running]
12: worker-6cd4bdb6b8-c6f5d [Running]
Specify the pod number [0]:
```
+286
View File
@@ -0,0 +1,286 @@
[KUBE](../README.md) &nbsp;/&nbsp; Mail server
# Mail server
## Template of zone
```zone
$TTL 300
@ IN SOA ns1.mydns.example. dnsadmin.mydomain.com. (
2025091001 ; serial
3600 ; refresh
900 ; retry
1209600 ; expire
300 ; minimum
)
IN NS ns1.mydns.example.
IN NS ns2.mydns.example.
; ===== A/AAAA =====
mta IN A {{PUBLIC_IPV4}}
; mta IN AAAA {{PUBLIC_IPV6}} ; if available
; if desired, client sites can resolve to the same IP
{{US1}} IN A {{PUBLIC_IPV4}}
{{US2}} IN A {{PUBLIC_IPV4}}
{{US3}} IN A {{PUBLIC_IPV4}}
; ===== MX =====
; @ IN MX 10 mta.{{ROOT_DOMAIN}}. ; the root domain also accepts mail, if needed
{{US1}} IN MX 10 mta.{{ROOT_DOMAIN}}.
{{US2}} IN MX 10 mta.{{ROOT_DOMAIN}}.
{{US3}} IN MX 10 mta.{{ROOT_DOMAIN}}.
; ===== SPF =====
; @ IN TXT "v=spf1 mx ~all" ; if available
mta IN TXT "v=spf1 a -all"
{{US1}} IN TXT "v=spf1 mx ~all"
{{US2}} IN TXT "v=spf1 mx ~all"
{{US3}} IN TXT "v=spf1 mx ~all"
; ===== DKIM =====
; For each customer domain, publish its own public key.
; The selector can be shared (e.g., selector1); keys are different.
selector1._domainkey.{{US1}} IN TXT "v=DKIM1; k=rsa; p={{PUBKEY_US1}}"
selector1._domainkey.{{US2}} IN TXT "v=DKIM1; k=rsa; p={{PUBKEY_US2}}"
selector1._domainkey.{{US3}} IN TXT "v=DKIM1; k=rsa; p={{PUBKEY_US3}}"
; ===== DMARC =====
; Initially p=none to collect reports without impacting delivery.
_dmarc.{{US1}} IN TXT "v=DMARC1; p=none; adkim=r; aspf=r; rua=mailto:{{DMARC_AGG}}; ruf=mailto:{{DMARC_FOR}}"
_dmarc.{{US2}} IN TXT "v=DMARC1; p=none; adkim=r; aspf=r; rua=mailto:{{DMARC_AGG}}; ruf=mailto:{{DMARC_FOR}}"
_dmarc.{{US3}} IN TXT "v=DMARC1; p=none; adkim=r; aspf=r; rua=mailto:{{DMARC_AGG}}; ruf=mailto:{{DMARC_FOR}}"
; It is recommended to set DMARC on the root domain to cover ALL subdomains with a single policy.
; _dmarc IN TXT "v=DMARC1; p=quarantine; sp=quarantine; adkim=r; aspf=r; rua=mailto:{{DMARC_AGG}}; ruf=mailto:{{DMARC_FOR}}"
; (if test mode first — replace p=none, sp=none)
; ===== Additionally (optional but useful) =====
; MTA-STS (if to implement)
;_mta-sts IN TXT "v=STSv1; id=2025-09-10"
;_smtp._tls IN TXT "v=TLSRPTv1; rua=mailto:tlsrpt@{{ROOT_DOMAIN}}"
;autoconfig IN CNAME autoconfig.mailhost.example. ; for client autoconfiguration
;autodiscover IN CNAME autodiscover.mailhost.example.
```
```zone
; ===== PTR =====
{{PUBLIC_IPV4}} → mta.{{ROOT_DOMAIN}}
```
Check: Postfix HELO/EHLO = mta.`{{ROOT_DOMAIN}}`
## Example
`{{ROOT_DOMAIN}}` → krumax.cz \
`{{PUBLIC_IPV4}}` → 31.31.73.67 \
`{{US1}}`/`{{US2}}`/`{{US3}}` → us1 / us2 / us3 \
`{{PUBKEY_US1}}`/`{{PUBKEY_US2}}`/`{{PUBKEY_US3}}` → DKIM public keys (only the content after `p=`, in a single line) \
`{{DMARC_AGG}}`/`{{DMARC_FOR}}` → Addresses for DMARC reports (for example, dmarc@krumax.cz)
```zone
$TTL 300
; ===== A =====
mta IN A 31.31.73.67
us1 IN A 31.31.73.67
us2 IN A 31.31.73.67
us3 IN A 31.31.73.67
; ===== MX =====
us1 IN MX 10 mta.krumax.cz.
us2 IN MX 10 mta.krumax.cz.
us3 IN MX 10 mta.krumax.cz.
; ===== SPF =====
mta IN TXT "v=spf1 a -all"
us1 IN TXT "v=spf1 mx ~all"
us2 IN TXT "v=spf1 mx ~all"
us3 IN TXT "v=spf1 mx ~all"
; ===== DKIM =====
selector1._domainkey.us1 IN TXT "v=DKIM1; k=rsa; p=MIIBI...(us1)"
selector1._domainkey.us2 IN TXT "v=DKIM1; k=rsa; p=MIIBI...(us2)"
selector1._domainkey.us3 IN TXT "v=DKIM1; k=rsa; p=MIIBI...(us3)"
; ===== DMARC =====
_dmarc.us1 IN TXT "v=DMARC1; p=none; adkim=r; aspf=r; rua=mailto:dmarc@krumax.cz; ruf=mailto:dmarc@krumax.cz"
_dmarc.us2 IN TXT "v=DMARC1; p=none; adkim=r; aspf=r; rua=mailto:dmarc@krumax.cz; ruf=mailto:dmarc@krumax.cz"
_dmarc.us3 IN TXT "v=DMARC1; p=none; adkim=r; aspf=r; rua=mailto:dmarc@krumax.cz; ruf=mailto:dmarc@krumax.cz"
```
```zone
; ===== PTR =====
31.31.73.67 → mta.krumax.cz
```
## Example of adding a new domain in Postfix
1. Adding the domain and generating DKIM
```bash
sudo kube.sh postfix add us2.krumax.cz
```
2. Retrieving DKIM
```bash
sudo kube.sh postfix dkim us2.krumax.cz
```
3. Adding DNS records:
```zone
us2 IN A 31.31.73.67
us2 IN MX 10 mta.krumax.cz.
selector1._domainkey.us2 IN TXT "v=DKIM1; k=rsa; p=MIIBI...(from step 2)"
_dmarc.us2 IN TXT "v=DMARC1; p=none; adkim=r; aspf=r; rua=mailto:dmarc@krumax.cz; ruf=mailto:dmarc@krumax.cz"
```
## Send mail in PHP.
Create file for test send mail `send-mail.php`
```php
<?
mb_internal_encoding('UTF-8');
$to = 'maksym.krugol@wedos.org';
$toName = 'Maksym Krugol';
$from = 'no-reply@us1.krumax.cz';
$fromName = 'Support team US1';
$return = 'bounce@us1.krumax.cz';
$subject = mb_encode_mimeheader('Test delivery (PHP)', 'UTF-8', 'B', "\r\n");
$bodyText = "Hi! Test with PHP.";
$bodyQP = quoted_printable_encode($bodyText);
$headers = [
"From: $fromName <$from>",
"Reply-To: $from",
"Return-Path: $return",
"Date: " . date('r'),
"Message-ID: <" . time() . "." . bin2hex(random_bytes(6)) . "@" . $hostname . ">",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=UTF-8",
"Content-Transfer-Encoding: quoted-printable",
"List-Unsubscribe: <mailto:unsubscribe@us1.krumax.cz?subject=unsubscribe>, <https://us1.krumax.cz/unsubscribe/".rawurlencode('maksym.krugol@wedos.org').">",
];
$headersStr = implode("\r\n", $headers);
$status = mail("$toName <$to>", $subject, $bodyQP, $headersStr, "-f$return");
echo $status ? "Success\n" : "Failed\n";
```
## Send mail in Wordpress.
1. Add Wordpress plugin `/wp-content/mu-plugins/smtp.php`
```php
<?php
// For 25 port (without TLS/login)
add_action('phpmailer_init', function ($phpmailer) {
$phpmailer->isSMTP();
$phpmailer->XMailer = 'krumaxMailer 1.0';
$phpmailer->Host = 'mta.krumax.cz';
$phpmailer->Port = 25;
$phpmailer->SMTPAuth = false;
$phpmailer->SMTPSecure = '';
$phpmailer->SMTPAutoTLS = false;
$phpmailer->From = 'no-reply@us1.krumax.cz';
// $phpmailer->FromName = 'Support team US1';
// $phpmailer->Hostname = 'us1.krumax.cz';
// $phpmailer->Encoding = 'quoted-printable';
// $phpmailer->Sender = 'bounce@us1.krumax.cz';
// $phpmailer->Timeout = 15;
});
```
```php
<?php
// For 587 port (with TLS/login)
add_action('phpmailer_init', function ($phpmailer) {
$phpmailer->isSMTP();
$phpmailer->XMailer = 'krumaxMailer 1.0';
$phpmailer->Host = 'mta.krumax.cz';
$phpmailer->Port = 587;
$phpmailer->Username = 'postmaster@us1.krumax.cz';
$phpmailer->Password = 'qwerty';
$phpmailer->SMTPAuth = true;
$phpmailer->SMTPSecure = 'tls';
$phpmailer->SMTPAutoTLS = true;
$phpmailer->SMTPOptions = [
'ssl' => [
'allow_self_signed' => true,
],
];
$phpmailer->From = 'no-reply@us1.krumax.cz';
// $phpmailer->FromName = 'Support team US1';
// $phpmailer->Hostname = 'us1.krumax.cz';
// $phpmailer->Encoding = 'quoted-printable';
// $phpmailer->Sender = 'bounce@us1.krumax.cz';
// $phpmailer->Timeout = 15;
});
```
2. Create file for test send mail `/send-mail.php`
```php
<?php
require_once 'wp-load.php';
$domain = "us1.krumax.cz";
$to = "maksym.krugol@wedos.org";
$toName = "Maksym Krugol";
$subject = "Test delivery (Wordpress)";
$message = "Hi! Test with Wordpress.";
$headers = [
"List-Unsubscribe: <mailto:unsubscribe@{$domain}?subject=unsubscribe>, <https://{$domain}/unsubscribe/{$to}>"
];
if (wp_mail("$toName <{$to}>", $subject, $message, $headers)) {
echo "Success";
} else {
echo "Failed";
}
```
3. Open URL http://us1.krumax.cz/send-mail.php
## Send mail from pod in k3s (use Postfix).
1. Install postfix: `apt-get install -y postfix`
2. Set config:
```bash
postconf -e 'myhostname = mta.krumax.cz'
postconf -e 'mydomain = us1.krumax.cz'
postconf -e 'myorigin = $mydomain'
```
3. Create file `test.mail`:
```
From: Support team US1 <no-reply@us1.krumax.cz>
To: Maksym Krugol <maksym.krugol@wedos.org>
Subject: Test delivery (postfix)
Date: Wed, 17 Sep 2025 10:53:13 +0200
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
Hi! Test with /usr/sbin/sendmail.
```
4. Send mail: `sendmail -v -oi -t -f 'no-reply@us1.krumax.cz' < test.mail`
## Send mail from pod in k3s (use msmtp).
1. Install msmtp: `apt-get install -y msmtp msmtp-mta ca-certificates`
2. Set config `/etc/msmtprc`:
```
defaults
auth on
tls on
tls_starttls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log
account default
host mta.krumax.cz
port 587
from no-reply@us1.krumax.cz
user postmaster@us1.krumax.cz
password qwerty
```
3. Create file `test.mail`:
```
From: Support team US1 <no-reply@us1.krumax.cz>
To: Maksym Krugol <maksym.krugol@wedos.org>
Subject: Test delivery (msmtp)
Date: Wed, 17 Sep 2025 10:53:13 +0200
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
Hi! Test with msmtp/sendmail.
```
4. Send mail: `sendmail -v -oi -t -f 'no-reply@us1.krumax.cz' < test.mail`
+47
View File
@@ -0,0 +1,47 @@
- [Getting started](#getting-started)
- [Site Creation](#site-creation)
- [Wordpress configuration](#wordpress-configuration)
- [Transfer](docs/transfer.md)
> [!NOTE]
> For `rSync` to work, the password must be in a file, with permissions `600` and owner `root`.
***
### `--info [resource]`
Extensive information on the status of various resources is provided:
- `[all]` - Info from all resources
- `k3s` - Info from all resources `k3s`
- `mariadb` - Info from `MariaDB` (master-slave replica)
- `redis` - Info from `Redis` and `RedisSentinel`
- `openlitespeed` - Info from `OpenLiteSpeed`
Example:
```bash
sudo kube.sh --info
sudo kube.sh --info all
sudo kube.sh --info mariadb
```
---
***
### `--version`
Collecting information about version system components:
- Kubernetes (k3s)
- MariaDB
- Redis
- OpenLiteSpeed
Example:
```bash
sudo kube.sh --version
```
---
+129
View File
@@ -0,0 +1,129 @@
[KUBE](../README.md) &nbsp;/&nbsp; Options
# Options
>Options in script `config.sh`
- `assetsPath` - Directory assets (for script)
- `dataPath` - Directory data (for script)
- `namespace` - Default kubernetes namespace
- `hostname` - Hostname server
- `basePath` - Base path for all resources
- `olsVhostsPath` - Directory for vhosts
- `domainsList` - File with domains list
***
- `k3sCmd` - Path to k3s on host
- `k3sReadyRetries` - Number of attempts to check k3s readiness
- `k3sReadySleep` - Pause between attempts
***
- `redisKube` - Redis identifier in k3s
- `redisSentinelKube` - Redis Sentinel identifier in k3s
- `redisYaml` - Path to file template YAML for Redis
- `redisPath` - Directory for Redis configuration
- `redisFileUsersAcl` - Filename for users in Redis
- `redisRootPass` - Redis password
***
- `mariadbKube` - MariaDB identifier in k3s
- `mariadbMasterKube` - MariaDB Master node identifier in k3s
- `mariadbSlaveKube` - MariaDB Slave node identifier in k3s
- `mariadbYaml` - Path to file template YAML for MariaDB
- `mariadbMasterPath` - Directory for MariaDB Master node (replica)
- `mariadbSlavePath` - Directory for MariaDB Slave node (replica)
- `mariadbRootPass` - MariaDB root password
***
- `olsKube` - Openlitespeed identifier in k3s
- `olsYaml` - Path to file template YAML for OpenLiteSpeed
- `olsPath` - Directory for OpenLiteSpeed configuration
- `olsAdminPath` - Directory for OpenLiteSpeed Admin configuration
- `olsConfigFile` - OpenLiteSpeed configuration file
- `olsVhostsConfigPath` - Directory for vhosts configuration files
- `olsAdminPass` - OpenLiteSpeed admin panel password
***
- `postfixKube` - Postfix identifier in k3s
- `postfixYaml` - Path to file template YAML for Postfix
- `postfixPath` - Directory for Postfix
- `postfixConfigPath` - Directory for Postfix configuration
- `postfixDkimPath` - Directory for Postfix DKIM
- `postfixTlsCrtFile` - File TLS certificate (if not specified, a self-signed one will be generated)
- `postfixTlsKey` - Fil TLS key (if not specified, a self-signed one will be generated)
- `postfixHost` - Host for MTA
- `postfixPostmaster` - Postmaster (email) for MTA
- `postfixDefaultRealm` - Default realm for MTA
- `postfixDkimSelector` - Selector DKIM
- `postfixDomainsFile` - File of Domains list
- `postfixAliasesFile` - File of Aliases list
- `postfixSendersFile` - File of Senders list
- `postfixIp` - IP address for MTA
***
- `workerKube` - Worker identifier in k3s
- `workerYaml` - Path to file template YAML for Worker
- `workerPath` - Directory for Worker
- `workerAgentPath` - Path to directory with agent script
- `workerTasksPath` - Path to directory with tasks-files
- `workerFilesPath` - Path to directory with loading files
- `workerName` - Worker name
- `workerApiUrl` - URL to API
- `workerApiGettingPause` - Pause between requests for a new task
- `workerApiSendingPause` - Pause between sending task statuses
***
- `logPath` - Directory journals (for script)
- `logFile` - Log file name format
***
- `backupPath` - Directory for backups on current host
- `backupLogPath` - Directory for backups logs
- `backupType` - Backup Type (rsync, archive)
- `backupFirst` - Directory name for the first backup of the day
- `backupDays` - Number of last days of backup storage (excluding current day backups)
- `backupMask` - A mask for selecting backup storage day directories (must match `scriptDate`)
- `backupLongTermPath` - Directory for long-term backups
- `backupLongTermDays` - Minimum number of days for a long-term backup.
- `backupExcludeFile` - List of files and directories that were excluded during backup
- `backupExcludeList` - List of files and directories that should be excluded during backup
- `backupFileSizeMin` - Minimum archive size during verification (bytes)
- `backupElapsedMax` - Maximum time elapsed since the last backup was created (min)
***
- `storagePath` - Directory for backups on external storage
- `storageType` - Directory external storage (rsync, ssh)
- `storageDays` - Number of last days of backup storage on external storage (excluding current day backups)
***
- `rsyncUri` - URI (format: `user@server[:port][/path]`) (for rSync)
- `rsyncPassFile` - File with password (for rSync)
- `rsyncPath` - Directory for backups on external storage (for rSync)
***
- `ftpHost` - Hostname (for FTP)
- `ftpPort` - Port (for FTP)
- `ftpUser` - Username (for FTP)
- `ftpPass` - Password (for FTP)
- `ftpPath` - Directory for backups on external storage (for FTP)
***
- `sshHost` - Hostname (for SSH)
- `sshUser` - Username (for SSH)
- `sshKeyFile` - Public key file (for SSH)
- `sshPath` - Directory for backups on external storage (for SSH)
***
- `reportFile` - Filename for reports
- `reportMask` - Report filename mask
***
- `mailTo` - Email for sending reports (to)
- `mailFrom` - Email for sending reports (from)
***
- `cronJobs` - Jobs for adding to CRON
***
- `diskUsedSpaceLimit` - Limiting the disk space used (%)
***
+167
View File
@@ -0,0 +1,167 @@
[KUBE](../../README.md) &nbsp;/&nbsp; [k3s](../k3s.md) &nbsp;/&nbsp; MariaDB
# Resource MariaDB
> [!NOTE]
> A replica of two pods `mariadb-master` and `mariadb-slave` is created. Each of the pods has a separate storage. When configuring the connection, the host is specified: `mariadb-master`.
- [Install](#install)
- [Remove](#remove)
- [Status](#status)
- [Info](#info)
- [Version](#version)
- [Database list](#database-list)
- [User list](#user-list)
- [Dump](#dump)
***
## Install
### Processing
1. Preparation.
- Recreate the secret with the passwords `root-password` and `replication-password`.
2. Creation.
- Prepare the import file.
- Import the YAML.
3. Post-processing.
- Initialize the replica.
Command:
```bash
sudo kube.sh --mariadb install
```
Example of log:
```bash
🔹[K3S] Resource add | mariadb
✅[MariaDB] | Delete old Secret: OK
✅[MariaDB] | Create new Secret: OK
🔹[MariaDB] Preparing /app/kube/assets/yaml/mariadb.yaml
🔹[K3S] Applying YAML /app/kube/data/yaml/mariadb.yaml
💡[K3S] Waiting for pods to be ready... | 2 not ready
💡[K3S] Waiting for pods to be ready... | 1 not ready
✅[MariaDB] Applied /app/kube/data/yaml/mariadb.yaml
🔹[MariaDB] Replica initialization...
✅[MariaDB] Master node | Create replication user: OK
✅[MariaDB] Master node | Status: OK
✅[MariaDB] Slave node | Change master: OK
✅[MariaDB] Slave node | Status: OK | Delay 0s
✅[MariaDB] Replica initialization completed successfully
```
***
## Remove
Command:
```bash
sudo kube.sh --mariadb remove
```
***
## Status
Command:
```bash
sudo kube.sh --mariadb status
```
Example of log:
```bash
🔹[MariaDB] Status
✅[MariaDB] Databases: 10 | Users: 10 | Size: 1.234 Gb
✅[MariaDB] Master node | Running
✅[MariaDB] Slave node | Running
```
***
## Info
Command:
```bash
sudo kube.sh --mariadb info
```
Example of log:
```bash
🔹[MariaDB] Info
🔹[MariaDB] Master node
mysql-bin.000025 10341
🔹[MariaDB] Slave node
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: mariadb-master
Master_User: replication_user
Master_Port: 3306
...
```
***
## Version
Command:
```bash
sudo kube.sh --mariadb version
```
Example of log:
```bash
🔹[MariaDB] Version
✅[MariaDB] Master node | mariadb from 11.7.2-MariaDB, client 15.2 for debian-linux-gnu (x86_64) using EditLine wrapper
✅[MariaDB] Slave node | mariadb from 11.7.2-MariaDB, client 15.2 for debian-linux-gnu (x86_64) using EditLine wrapper
```
***
## Database list
> [!NOTE]
> List of databases (except for service databases: `information_schema`, `performance_schema`, `mysql`, `sys`)
Command:
```bash
sudo kube.sh --mariadb database
```
Example of log:
```bash
🔹[MariaDB] Database list
us1_example_com
us2_example_com
us3_example_com
```
***
## User list
> [!NOTE]
> List of users (except for service users `root`, `replication_user`)
Command:
```bash
sudo kube.sh --mariadb user
```
Example of log:
```bash
🔹[MariaDB] User list
us1_example_com
us2_example_com
us3_example_com
```
***
## Dump
Command:
```bash
sudo kube.sh --mariadb dump
sudo kube.sh --mariadb dump dump.sql
sudo kube.sh --mariadb dump dump.sql us1_example_com
```
Example of log:
```bash
🔹[MariaDB] Dump
✅[MariaDB] Dump: /app/kube/data/mariadb/2025-10-27_10-10-58.sql.tar.gz
```
@@ -0,0 +1,179 @@
[KUBE](../../README.md) &nbsp;/&nbsp; [k3s](../k3s.md) &nbsp;/&nbsp; OpenLiteSpeed
# Resource OpenLiteSpeed
> [!NOTE]
> Two (you can specify a different number) pods are created. They work simultaneously. If one pod fails, the second pod starts processing all requests until a replacement for the failed pod is brought up. When changes are made to the virtual host configuration, OpenLiteSpeed is restarted sequentially in all pods.
>
> The administration panel OpenLiteSpeed is available at an address:
> - `<domain>:31080` (local and remote)
> - `<server ip>:31080` (remote)
> - `<node ip>:7080` (local, `<node ip>` can be found upon request: `kube.sh --info | grep 7080`)
- [Install](#install)
- [Remove](#remove)
- [Status](#status)
- [Info](#info)
- [Version](#version)
- [Restart](#restart)
- [Site list](#site-list)
- [Site unlock](#site-unlock)
- [Site lock](#site-lock)
- [Config](#config-test)
***
## Install
### Processing
1. Creation.
- Prepare the import file.
- Import the YAML.
2. Post-processing.
- Initialization.
Command:
```bash
sudo kube.sh --ols install
```
Example of log:
```bash
🔹[OpenLiteSpeed] Install
🔹[OpenLiteSpeed] Preparing /app/kube/assets/yaml/openlitespeed.yaml
🔹[K3S] Applying YAML /app/kube/data/yaml/openlitespeed.yaml
💡[K3S] Waiting for pods to be ready... | 1 not ready
💡[K3S] Waiting for pods to be ready... | 1 not ready
✅[OpenLiteSpeed] Applied /app/kube/data/yaml/openlitespeed.yaml
🔹[OpenLiteSpeed] Initialization...
🔹[OpenLiteSpeed] Authorization parameters updated
service/traefik patched
🔹[OpenLiteSpeed] Pod: openlitespeed-0 | Restart...
🔹[OpenLiteSpeed] Pod: openlitespeed-1 | Restart...
✅[OpenLiteSpeed] Initialization completed successfully
```
***
## Remove
Command:
```bash
sudo kube.sh --ols remove
```
***
## Status
Command:
```bash
sudo kube.sh --ols status
```
Example of log:
```bash
🔹[OpenLiteSpeed] Status
✅[OpenLiteSpeed] openlitespeed-0 | Running | PID 251
✅[OpenLiteSpeed] openlitespeed-1 | Running | PID 216
```
***
## Info
Command:
```bash
sudo kube.sh --ols info
```
Example of log:
```bash
🔹[OpenLiteSpeed] Info
🔹[OpenLiteSpeed] openlitespeed-0
litespeed is running with PID 251.
🔹[OpenLiteSpeed] openlitespeed-1
litespeed is running with PID 216.
```
***
## Version
Command:
```bash
sudo kube.sh --ols version
```
Example of log:
```bash
🔹[OpenLiteSpeed] Version
✅[OpenLiteSpeed] openlitespeed-0 | LiteSpeed/1.8.3 Open (BUILD built: Fri Feb 28 16:33:02 UTC 2025)
✅[OpenLiteSpeed] openlitespeed-0 | PHP: 8.3.17
✅[OpenLiteSpeed] openlitespeed-1 | LiteSpeed/1.8.3 Open (BUILD built: Fri Feb 28 16:33:02 UTC 2025)
✅[OpenLiteSpeed] openlitespeed-1 | PHP: 8.3.17
```
***
## Restart
Command:
```bash
sudo kube.sh --ols restart
```
Example of log:
```bash
🔹[OpenLiteSpeed] Restart
🔹[OpenLiteSpeed] Pod: openlitespeed-0 | Restart...
🔹[OpenLiteSpeed] Pod: openlitespeed-1 | Restart...
```
***
## Site list
Command:
```bash
sudo kube.sh --ols list [option]
```
Options:
- all - All sites (colored up/down) (default)
- all - All sites
- up - Unlocked sites
- down - Locked sites
Example of log:
```bash
example1.com
example2.com
example3.com
```
***
## Site unlock
> [!NOTE]
> Unlock domain (Resume site operation in OpenLiteSpeed).
Command:
```bash
sudo kube.sh --ols up <domain>
```
***
## Site lock
> [!NOTE]
> Lock domain (Pause the site in OpenLiteSpeed).
>
> The OpenLiteSpeed page will be displayed with a 403 error when the domain is accessed.
Command:
```bash
sudo kube.sh --ols down <domain>
```
***
## Config test
Command:
```bash
sudo kube.sh --ols config
```
+121
View File
@@ -0,0 +1,121 @@
[KUBE](../../README.md) &nbsp;/&nbsp; [k3s](../k3s.md) &nbsp;/&nbsp; Postfix
# Resource Postfix
- [Install](#install)
- [Remove](#remove)
- [Status](#status)
- [Add domain](#add-domain-)
- [Get DKIM key](#get-dkim-key-for-dns-record-domain-or-dkim-record-lists)
- [Check DNS records](#check-dns-records-for--or-mta)
- [Get template of DNS records](#get-template-of-dns-records)
***
## Install
Command:
```bash
sudo kube.sh --postfix install
```
Example of log:
```bash
🔹[Postfix] Install
🔹[Postfix] Preparing /app/kube/assets/yaml/postfix.yaml
🔹[K3S] Applying YAML /app/kube/data/yaml/postfix.yaml
💡[K3S] Waiting for pods to be ready... | 1 not ready
💡[K3S] Waiting for pods to be ready... | 1 not ready
💡[K3S] Waiting for pods to be ready... | 1 not ready
💡[K3S] Waiting for pods to be ready... | 1 not ready
✅[Postfix] Applied /app/kube/data/yaml/postfix.yaml
```
***
## Remove
Command:
```bash
sudo kube.sh --postfix remove
```
***
## Status
> [!NOTE]
> In progress...
Command:
```bash
sudo kube.sh --postfix status
```
Example of log:
```bash
???
```
***
## Add domain <domain>
> [!NOTE]
> In progress...
Command:
```bash
sudo kube.sh --postfix add <domain>
```
***
## Get DKIM key for DNS record domain or DKIM record lists
Command:
```bash
sudo ube.sh --postfix dkim [domain]
```
Example of log:
```bash
[Postfix] DKIM key for DNS
selector1._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; s=email; "
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6rCyqEvQ1...FQIDAQAB" ) ; ----- DKIM key selector1 for krumax.cz
```
***
## Check DNS records for <domain> or MTA
Command:
```bash
sudo kube.sh --postfix dns [domain]
```
Example of log:
```bash
🔹[Postfix] Check DNS records
✅[Postfix] A record: mta.krumax.cz | 31.31.73.67
✅[Postfix] SPF record: mta.krumax.cz | "v=spf1 a -all"
✅[Postfix] PTR record: mta.krumax.cz | mta.krumax.cz.
```
***
## Get template of DNS records
Command:
```
sudo kube.sh --postfix template
```
Example of log:
```bash
🔹[Postfix] Template
🔹[Postfix] For MTA ==============
🔹[Postfix] IN A 31.31.73.67
🔹[Postfix] IN TXT v=spf1 a -all
🔹[Postfix] PTR -> mta.krumax.cz
🔹[Postfix] For site =============
🔹[Postfix] IN A 31.31.73.67
🔹[Postfix] IN MX 10 mta.krumax.cz.
🔹[Postfix] IN TXT v=spf1 mx ~all
🔹[Postfix] IN TXT v=DKIM1; ...
🔹[Postfix] IN TXT v=DMARC1; ...
```
+163
View File
@@ -0,0 +1,163 @@
[KUBE](../../README.md) &nbsp;/&nbsp; [k3s](../k3s.md) &nbsp;/&nbsp; Redis
# Resource Redis
> [!NOTE]
> A replica of three pods is created: 1 master (`redis-0`) + 2 slaves (`redis-1`, `redis-2`). A cluster of three RedisSentinel is created to manage the replica. When configuring the connection, the host is specified: redis-0.redis.
> [!WARNING]
> In the current configuration, when changing the RedisSentinel master node of the replica, there is no automatic change of connection to the new master node. Solution: adding HAProxy to automatically redirect requests to the current Redis master node.
- [Install](#install)
- [Remove](#remove)
- [Status](#status)
- [Info](#info)
- [Version](#version)
- [User list](#user-list)
***
## Install
### Processing
1. Preparation.
- Recreate the secret with the passwords `root-password`.
2. Creation.
- Prepare the import file.
- Import the YAML.
Command:
```bash
sudo kube.sh --redis install
```
Example of log:
```bash
🔹[Redis] Install
✅[Redis] Delete old Secret: OK
✅[Redis] Create new Secret: OK
🔹[Redis] Preparing /app/kube/assets/yaml/redis.yaml
🔹[K3S] Applying YAML /app/kube/data/yaml/redis.yaml
💡[K3S] Waiting for pods to be ready... | 2 not ready
💡[K3S] Waiting for pods to be ready... | 1 not ready
✅[Redis] Applied /app/kube/data/yaml/redis.yaml
```
***
## Remove
Command:
```bash
sudo kube.sh --redis remove
```
***
## Status
Command:
```bash
sudo kube.sh --redis status
```
Example of log:
```bash
🔹[Redis] Status
✅[Redis] redis-0 | Role: Master | Slaves: 2
✅[Redis] redis-1 | Role: Slave | Master: redis-0.redis
✅[Redis] redis-2 | Role: Slave | Master: redis-0.redis
✅[RedisSentinel] redis-sentinel-0 | Master: mymaster [redis-0.redis:6379] | Slaves: 2 | Other sentinels: 2 | Quorum: 2
✅[RedisSentinel] redis-sentinel-0 | Slave: 10.42.0.16:6379 [10.42.0.16:6379] | Master: redis-0.redis
✅[RedisSentinel] redis-sentinel-0 | Slave: 10.42.0.14:6379 [10.42.0.14:6379] | Master: redis-0.redis
✅[RedisSentinel] redis-sentinel-1 | Master: mymaster [redis-0.redis:6379] | Slaves: 2 | Other sentinels: 2 | Quorum: 2
✅[RedisSentinel] redis-sentinel-1 | Slave: 10.42.0.16:6379 [10.42.0.16:6379] | Master: redis-0.redis
✅[RedisSentinel] redis-sentinel-1 | Slave: 10.42.0.14:6379 [10.42.0.14:6379] | Master: redis-0.redis
✅[RedisSentinel] redis-sentinel-2 | Master: mymaster [redis-0.redis:6379] | Slaves: 2 | Other sentinels: 2 | Quorum: 2
✅[RedisSentinel] redis-sentinel-2 | Slave: 10.42.0.16:6379 [10.42.0.16:6379] | Master: redis-0.redis
✅[RedisSentinel] redis-sentinel-2 | Slave: 10.42.0.14:6379 [10.42.0.14:6379] | Master: redis-0.redis
```
***
## Info
Command:
```bash
sudo kube.sh --redis info
```
Example of log:
```bash
🔹[Redis] Info
🔹[Redis] redis-0
# Replication
role:master
connected_slaves:2
slave0:ip=10.42.0.14,port=6379,state=online,offset=1092850,lag=0
slave1:ip=10.42.0.16,port=6379,state=online,offset=1092714,lag=1
...
🔹[Redis] redis-1
# Replication
role:slave
master_host:redis-0.redis
master_port:6379
master_link_status:up
...
🔹[Redis] redis-2
# Replication
role:slave
master_host:redis-0.redis
master_port:6379
master_link_status:up
...
🔹[RedisSentinel] redis-sentinel-0
# Sentinel
sentinel_masters:1
...
master0:name=mymaster,status=ok,address=redis-0.redis:6379,slaves=2,sentinels=3
🔹[RedisSentinel] redis-sentinel-1
# Sentinel
sentinel_masters:1
...
master0:name=mymaster,status=ok,address=redis-0.redis:6379,slaves=2,sentinels=3
🔹[RedisSentinel] redis-sentinel-2
# Sentinel
sentinel_masters:1
...
master0:name=mymaster,status=ok,address=redis-0.redis:6379,slaves=2,sentinels=3
```
***
## Version
Command:
```bash
sudo kube.sh --redis version
```
Example of log:
```bash
🔹[Redis] Version
✅[Redis] redis-0 | 7.4.2
✅[Redis] redis-1 | 7.4.2
✅[Redis] redis-2 | 7.4.2
✅[RedisSentinel] redis-sentinel-0 | 7.4.2
✅[RedisSentinel] redis-sentinel-1 | 7.4.2
✅[RedisSentinel] redis-sentinel-2 | 7.4.2
```
***
## User list
Command:
```bash
sudo kube.sh --redis user
```
Example of log:
```bash
🔹[Redis] User list
default
us1_example_com
us2_example_com
us3_example_com
```
+121
View File
@@ -0,0 +1,121 @@
[KUBE](../../README.md) &nbsp;/&nbsp; [k3s](../k3s.md) &nbsp;/&nbsp; Worker
# Resource Transfer
- [Install](#install)
- [Remove](#remove)
- [Transfer site](#transfer-site)
- [Pause for Agent to receive new tasks from the API](#pause-for-agent-to-receive-new-tasks-from-the-api)
- [Removing the pause for Agent to receive new tasks from the API](#removing-the-pause-for-agent-to-receive-new-tasks-from-the-api)
- [Tasks list](#tasks-list)
- [Update ENV](#update-env)
***
## Install
Command:
```bash
sudo kube.sh --worker install
```
Example of log:
```bash
🔹[Worker] Install
🔹[Worker] Preparing /app/kube/assets/yaml/worker.yaml
🔹[K3S] Applying YAML /app/kube/data/yaml/worker.yaml
💡[K3S] Waiting for pods to be ready... | 1 not ready
💡[K3S] Waiting for pods to be ready... | 1 not ready
```
***
## Remove
Command:
```bash
sudo kube.sh --Worker remove
```
***
## Execute task
Command:
```bash
sudo kube.sh --worker task <file> [domain]
```
Example of log:
```bash
🔹[Worker] Task: /_data/worker/tasks/6d5b3169-a15f-4007-8cc7-ebfc8d75e3b2.task
🔹[Worker] Action: test
🔹[Worker] Database URL: https://wp.krumax.cz/transfer_36b91e68-53d3-49ac-922a-0031e664125b/0bf26901-c12d-4015-9bbe-cefc5c1a92d6.sql.zip
🔹[Worker] Files URL: https://wp.krumax.cz/transfer_36b91e68-53d3-49ac-922a-0031e664125b/0bf26901-c12d-4015-9bbe-cefc5c1a92d6.zip
🔹[Worker] Download archive database --> /app/transfers/us00.krumax.cz/us00.krumax.cz.sql.zip
🔹[Worker] Download archive files --> /app/transfers/us00.krumax.cz/us00.krumax.cz.zip
🔹[Worker] Create site: us00.krumax.cz
🔹Files source: /app/transfers/us00.krumax.cz/us00.krumax.cz.zip
🔹Database source: /app/transfers/us00.krumax.cz/us00.krumax.cz.sql.zip
🔹[OpenLiteSpeed] Pod: openlitespeed-0 | Restart...
🔹[OpenLiteSpeed] Pod: openlitespeed-1 | Restart...
✅[Worker] Action: test | Success
```
***
## Tasks list
Command:
```bash
sudo kube.sh --worker list
```
Example of log:
```bash
🔹[Worker] Task list
# | Task | Action | Status | Time, sec
---------------------------------------------------------------------------
1 | d580040f-b3b8-43e1-af7a-8e35c80a688c | test | new | ?
2 | pause | pause | execution | 691175
```
***
## Pause for Agent to receive new tasks from the API (pause)
Command:
```bash
sudo kube.sh --worker down
```
Example of log:
```bash
🔹[Worker] Put on pause...
```
***
## Removing the pause for Agent to receive new tasks from the API (unpause)
Command:
```bash
sudo kube.sh --worker up
```
Example of log:
```bash
🔹[Worker] Resuming from pause...
```
***
## Reload
Command:
```bash
sudo kube.sh --worker reload
```
Example of log:
```bash
🔹[Worker] Reload...
🔹[Worker] Updated ENV:
API_URL: http://api.sodew.ai/api/tasks
WORKER_ID: worker-dev
GETTING_PAUSE: 30
SENDING_PAUSE: 15
```
+73
View File
@@ -0,0 +1,73 @@
[KUBE](../README.md) &nbsp;/&nbsp; Restore
# Restore
The restore process is divided into three stages:
1. Restoring website files from the `<domain>` directory or the `<domain>.tar.gz` archive.
2. Restoring the `<domain>.conf` file.
3. Restoring the database from the `<domain>.sql` file or the `<domain>.sql.tar.gz` archive.
> [!WARNING]
> If an error occurs at any stage, the process does not stop.
> [!WARNING]
> Files, databases, and other resources at each stage are either pre-cleaned or immediately overwritten without confirmation.
## Commands
### `-r, --restore <domain> [action]`
Restore site files and database from backup for a `domain`.
Example:
```bash
sudo kube.sh -r example.com
```
The source of resources for recovery is the directories defined in `backupPath` and `backupLongTermPath`.\
In the backup folder is searched for 3 types of resources:
- `<source path>/<date>/<marker>/<domain>` - directory with site files (`file:d`)
- `<source path>/<date>/<marker>/<domain>.tar.gz` - site file archive (`file:a`)
- `<source path>/<date>/<marker>/<domain>.sql` - site database SQL-file (`db:f`)
- `<source path>/<date>/<marker>/<domain>.sql.tar.gz` - site database archive (`db:a`)
- `<source path>/<date>/<marker>/<domain>.conf` - site database archive (`conf`)
The search results in a list of format: `<counter>: <data> <marker> [<list of resource>]`. Then you should specify the number of the selected marker for restore.
> [!NOTE]
> If one resource is selected (`file:d`/`file:a`/`db:f`/`db:a`/`conf`), only one resource will be restored. The other will remain unchanged.
> [!NOTE]
> If there's a `file:d` and a `file:a`. Priority is given to `file:d`. If there's a `db:f` and a `db:a`. Priority is given to `db:f`.
> [!NOTE]
> Once database are restored, the Redis keys for the domain are deleted.
Example:
```bash
1: 2025-04-29 14-22-22 [file:d file:a db:f conf]
2: 2025-04-29 14-20-20 [file:d db:a]
3: 2025-04-29 12-00-00 [file:a conf]
4: 2025-04-29 _first [db:a]
```
You can specify an additional parameter after the domain:
- `list` - will display a list of available markers for restore
- `last` - automatic site restore from the `last backup`
- `full` - automatic site restore from the last `FULL backup`
- `auto` - automatic site restore from the last `FULL backup` or the `last backup`
- `N` - automatic site restore from the `last backup #N` (N: 1+)
`FULL backup` is a backup that contains a copy of the site files, a copy of the database, and a copy of the .conf file\
`last backup #N` marker number (starting from 1) in the list sorted by creation time (can be seen with the list command)
Example:
```bash
sudo kube.sh -r example.com list
sudo kube.sh -r example.com last
sudo kube.sh -r example.com auto
sudo kube.sh -r example.com 3
```
> [!WARNING]
> Restoring from the last full copy will be done without question.
+24
View File
@@ -0,0 +1,24 @@
[KUBE](../README.md) &nbsp;/&nbsp; Script
# Script
> A set of scripts to execute.
## Commands
### `--script <script>`
Script:
- `backup` - Creating a backup (within a day) of all sites and then synchronizing the backups (within a day) with external storage.
- `backup-clean` - Cleanup of old backups on the current host and on external storage.
- `backup-long-term` - Create/update a long-term backup.
- `report-backup-last` - Report on creating the last backup.
- `report-backup-day` - Backup report for the current day.
- `report-status` - Creating and sending a report on the status of k3s and its nodes
- `mariadb-dump` - Creating a full backup (of all databases) of MariaDB
- `worker-observer` - Launching the task observer
Example:
```bash
sudo kube.sh --script backup
```
+36
View File
@@ -0,0 +1,36 @@
[KUBE](../README.md) &nbsp;/&nbsp; Shell
# Shell
> Opening the shell in the pods.
## Commands
### `--shell [cli]`
> [!NOTE]
> Specifying `cli` will open the appropriate CLI where possible (`MariaDB`, `Redis`, `Redis Sentinel`)
Example:
```bash
sudo kube.sh --shell
sudo kube.sh --shell cli
```
Examples of responses:
```bash
🔹Shell
1: mariadb-master-0 [Running]
2: mariadb-slave-0 [Running]
3: openlitespeed-0 [Running]
4: openlitespeed-1 [Running]
5: postfix-78c47b95f6-42jcq [Running]
6: redis-0 [Running]
7: redis-1 [Running]
8: redis-2 [Running]
9: redis-sentinel-0 [Running]
10: redis-sentinel-1 [Running]
11: redis-sentinel-2 [Running]
12: worker-6cd4bdb6b8-c6f5d [Running]
Specify the pod number [0]:
```
+191
View File
@@ -0,0 +1,191 @@
[KUBE](../README.md) &nbsp;/&nbsp; Site
# Site
## Site Creation
Stages of Site Creation:
### 1. Configuration.
The configuration, as a file (`<dataPath>/config/<domain>.config`), contains the connection parameters for `MariaDB` and `Redis`. The configuration can be prepared before starting the site creation or will be generated automatically.
Example configuration file:
```
databaseName=example_com
databaseUser=example_com
redisUser=example_com
databasePass=qwerty
redisPass=qwerty
```
Fields not specified will be generated automatically. The names of the database, database user, and `Redis` user are generated based on the domain using the functions `mariadbDomain2id` and `domain2redis`.
### 2. Initial Check.
Before starting the site creation, some configuration checks are performed:
* Check for absence of the vhost directory for the domain (`/path/to/vhosts/example.com`)
* Check for absence of the domain entry in the `OpenLiteSpeed` configuration
* Check for absence of the specified database
* Check for absence of the specified database user
* Check for absence of the specified `Redis` user
### 3. Distribution Check.
When creating a site, you can specify the source for the site files as `pathF` and the source for the database as `pathD`. `pathF` can be a directory or an archive file. `pathD` can be a SQL file for import as is, or an archive file.
If a file source `pathF` is specified, its presence is checked. Otherwise, the default distribution is used:
* for `Wordpress`: the directory `<assetsPath>/vhost.wordpress` (if the directory is missing, the file `<assetsPath>/vhost.wordpress.tar.gz` is used)
* for `non-Wordpress`: the directory `<assetsPath>/vhost.default` (if the directory is missing, the file `<assetsPath>/vhost.default.tar.gz` is used)
If the specified data sources are not found, or the default resources are not found, the process is aborted.
### 4. Site Creation.
When a site is created, the following steps occur:
* creation of the site directory structure `<vhostPath>/<domain>/{www,tmp,session}`
* copying site files from the source to the site directory
* creation of an OS user for the site files and changing access rights
* creation of a record in the `OpenLiteSpeed` configuration
* creation of a database and database user in `MariaDB`
* creation of a `Redis` user
* importing the database from the source, if specified
* writing database connection parameters (for `Wordpress`)
* writing Redis connection parameters (for `Wordpress`)
* writing to hosts (?!)
* restarting `OpenLiteSpeed`
***
## Site on Wordpress creation
* MariaDB
> The database name and database user name are formed on the basis of domain conversion, where all `.-` are replaced by `_`. If necessary, the conversion rule can be changed (mariadbDomain2id function). When generating connection parameters, the database password is saved to the `data/<domain>.mariadb` file.
Example for `example.com`:
```php
define( 'DB_HOST', 'mariadb-master' );
define( 'DB_NAME', 'example_com' );
define( 'DB_USER', 'example_com' );
define( 'DB_PASSWORD', 'qwerty' );
```
* Redis
> Redis username is generated based on domain conversion, where all `.-` are replaced by `_`. The conversion rule can be changed if necessary (`domain2redis` function). When generating connection parameters, the database password is stored in the `data/<domain>.redis` file.
Example for `example.com`:
```php
define( 'WP_REDIS_HOST', 'redis-0.redis' );
define( 'WP_REDIS_PORT', '6379' );
define( 'WP_REDIS_PASSWORD', ['example_com', 'qwerty'] );
define( 'WP_REDIS_PREFIX', 'example_com:' );
```
> [!NOTE]
> A separate Redis user is created for each Wordpress and a PrefixKey is also specified to separate keys in Redis.
***
## Commands
### `--site add <domain> [pathF] [pathD]`
Adding a site
Example:
```bash
sudo kube.sh --site add example.com
sudo kube.sh --site add example.com /path/to/files /path/to/database
```
***
### `--site wp <option>`
Add site(s) on Wordpress. Option:
- `list` - add list of domains with Wordpress from a default file: (`domainsList` in `config.sh`)
- `<file>` - add list of domains with Wordpress from a file (e.g., `/path/to/file.txt`)
- `<domain>` [pathF] [pathD] - add a domain with Wordpress (e.g., `example.com`)
Example:
```bash
sudo kube.sh --site wp /path/to/file.txt
sudo kube.sh --site wp list
sudo kube.sh --site wp example.com
sudo kube.sh --site wp example.com /path/to/files /path/to/database
```
***
### `--site remove <domain>`
> [!WARNING]
> Site remove (site directories, site database, `OpenLiteSpeed` configuration entries).
Example:
```bash
sudo kube.sh --remove example.com
```
***
### `--site status [domain]`
When specifying a domain, the following checks are performed:
* Presence of the domain in the `OpenLiteSpeed` configuration
* Presence of the domain in the list of stopped sites (OpenLiteSpeed)
* Presence of the domain directory in the virtual hosts directory (`olsVhostsPath`)
* Presence of the `WordPress` configuration file (`wp-config.php`)
* Checking the database name entry in the WordPress configuration and verifying its existence
* Checking the database user entry in the WordPress configuration and verifying its existence
* Checking database connectivity (retrieving the list of tables in the database)
* Checking the Redis user entry in the `WordPress` configuration and verifying its existence
* Checking connectivity to `Redis` (using the `PING` command)
* Checking the site's HTTP response
If the domain is not specified, the following checks are performed for all domains:
* Presence of the domain in the list of stopped sites (`OpenLiteSpeed`)
* Presence of the domain directory in the virtual hosts directory (`olsVhostsPath`)
* Presence of the `WordPress` configuration file (`wp-config.php`)
* Checking the database name entry in the `WordPress` configuration and verifying its existence
* Checking the database user entry in the `WordPress` configuration and verifying its existence
* Checking the `Redis` user entry in the `WordPress` configuration and verifying its existence
Example:
```bash
sudo kube.sh --site
sudo kube.sh --site example.com
```
Examples of responses:
```bash
🔹Site | example.com
✅example.com | Found in OpenLiteSpeed configuration
✅example.com | Directory: /_data/vhosts/example.com
✅example.com | Wordpress config: /_data/vhosts/example.com/www/wp-config.php
✅example.com | MariaDB database: example_com
✅example.com | MariaDB user: example_com
✅example.com | MariaDB tables: 10
✅example.com | Redis user: example_com
✅example.com | Redis ping: PONG
✅example.com | Site response: 200
```
```bash
🔹Site | all
## | Domain | CFG | DIR | OLS | MD | MU | RU | WP
--------------------------------------------------------------------------------
1 | example1.com | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅
2 | example2.com | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅
3 | example3.com | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅
4 | example4.com | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅
5 | example5.com | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅
6 | example6.com | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅
7 | example7.com | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | --
8 | example8.com | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | --
9 | Example | --- | --- | ✅ | -- | -- | -- | --
```
Where:
- `CFG` - existence of the configuration file `$dataPath/config/<domain>.config`
- `DIR` - existence of the directory `$olsVhostsPath/<domain>`
- `OLS` - existence of an entry in the `OpenLiteSpeed` configuration
- `MD` - existence of the database (based on the entry in the configuration file)
- `MU` - existence of the database user (based on the entry in the configuration file)
- `RU` - existence of the Redis user (based on the entry in the configuration file)
- `WP` - existence of the WP configuration file `$olsVhostsPath/<domain>/www/wp-config.php`
)
+23
View File
@@ -0,0 +1,23 @@
[KUBE](../README.md) &nbsp;/&nbsp; Storage
# Storage
## Commands
### `-s, --storage [option]`
Copying backups to external storage. Options:
- `rsync` - The backup directory `<backup path>/<current date>` is synchronized to the specified address `rsyncUri` using `rSync`.
- `ssh` - The backup directory `<backup path>/<current date>` is synchronized to the specified address `sshHost` using `rSync`.
> [!NOTE]
> The default value (`rsync` or `ssh`) is set by the `storageType` variable in the configuration file `config.sh`.
> [!NOTE]
> For correct operation it is necessary to set up SSH-keys for authentication on the external storage. Create a directory (specified in `sshPath`) and grant necessary user rights on the external storage.
Example:
```bash
sudo kube.sh -s
sudo kube.sh -s rsync
sudo kube.sh -s ssh
```
+24
View File
@@ -0,0 +1,24 @@
[KUBE](../README.md) &nbsp;/&nbsp; Test
# Test
> [!NOTE]
> Testing the operation of individual units or the entire system.
## Commands
### `--test <option>`
Options:
- `mariadb` - A database with a random name is created on the master device and the existence of the created database is checked on the slave device.
- `redis` - A key with a random name is created on the master and the existence of the created key is verified on the replicas.
- `site` - A site is created (without `WordPress`). MariaDB (a database and user are created). `Redis` (a user is created). The site is opened via HTTP (the response code is checked). The site is deleted.
- `wordpress` - A WordPress site is created. `MariaDB` (a database and user are created). `Redis` (a user is created). The site is opened via HTTP (the response code is checked). The site is deleted.
- `mailint` - Internal test of sending a letter from one user to another.
- `mailext` - Test sending a letter to the specified address (In progress...)
Example:
```bash
sudo kube.sh --test mariadb
sudo kube.sh --test redis
sudo kube.sh --test mailint
```
+214
View File
@@ -0,0 +1,214 @@
[KUBE](../README.md) &nbsp;/&nbsp; Transfer sites
# Transfer sites
> [!NOTE]
> In progress...
- [Transfer via console](#transfer-via-console)
- [Transfer via plugin Wordpress](#transfer-via-plugin-worpress)
- [Workflow 1. Cloning a website via the WordPress plugin](#workflow-1-cloning-a-website-via-the-wordpress-plugin)
- [Workflow 2. Cloning websites to SODEW servers via the WordPress plugin](#workflow-2-cloning-websites-to-sodew-servers-via-the-wordpress-plugin)
- [Workflow 3. Detailed description of the process](#workflow-3-detailed-description-of-the-process)
## Transfer via console:
```bash
sudo kube.sh --worker task </path/to/configFile> [domain]
```
### Configuration analysis.
Example:
```ini
action=test
files_url=https://wp.krumax.cz/transfer_36b91e68-53d3-49ac-922a-0031e664125b/2b3d170e-9f50-435c-b189-b8c3a45cbb8a.zip
database_url=https://wp.krumax.cz/transfer_36b91e68-53d3-49ac-922a-0031e664125b/2b3d170e-9f50-435c-b189-b8c3a45cbb8a.sql.zip
domain=new.domain.com
status=new
```
Where:
* `action` — the main operation (`copy`/`test`/`migrate`/...)
* `files_url` — link to the files archive
* `database_url` — link to the DB archive
* `status` — execution status:
* `new` - new task
* `execution` - in progress
* `success` - task completed successfully
* `failed` - task failed
* `domain` - new domain (optional field)
> [!NOTE]
> Working only action `test` (18.11.2025).
> [!NOTE]
> If a domain is specified in the command call, the domain from the configuration file is ignored.
> [!NOTE]
> If the domain is not specified in the command and not specified in the configuration file, it will be assigned automatically from the available ones (`domainsList`). If none are available, the task fails.
### Work
**Stage 1**. Configuration analysis.
**Stage 2**. Availability check and archive download.
The download is performed into the folder `<transferPath>/<domain>/`:
* `<transferPath>/<domain>/<domain>.zip` — files archive
* `<transferPath>/<domain>/<domain>.sql.zip` — DB archive
**Stage 3**. Depending on the task:
* `test` - A site is created based on the downloaded backup.
* `copy` - in progress...
* `migrate` - in progress...
**Stage 4**. The status is changed in the configuration file
* `success` - upon successful completion of the task
* `failed` - upon errors at any stage
## Transfer via plugin Wordpress
When using the plugin "SODEW Backup & Transfer" ([https://gitlab.wedos.org/mk250/transfer-plugin](https://gitlab.wedos.org/mk250/transfer-plugin)), it is possible to copy a site to SODEW hosting.
### Preparation
**Stage 1**. Create a full site backup in the plugin.
**Stage 2**. Create a task "Launch a copy of the website on SODEW hosting".
### Work
**Stage 1**. Sending a request to create a copy of the site for testing to `api.sodew.ai`. Request parameters:
* `action` – selected action
* `transfer_id` – plugin identifier
* `backup_id` – backup identifier
* `base_url` – site URL (WordPress)
* `base_path` – base path
Example:
```json
[
"action":"test",
"transfer_id":"22222222-53d3-49ac-922a-0031e664125b",
"backup_id":"33333333-c976-43c2-bdee-d7d6ec21900a",
"base_url":"https://wp.us1.com",
"base_path":"/usr/www/wp.us1.com",
]
```
**Stage 2**. Processing the request on the `api.sodew.ai` side.
1. Parameter validation
2. Preparation and validation of URLs to the backup:
```ini
<base_url>/transfer_<transfer_id>/<backup_id>.zip
<base_url>/transfer_<transfer_id>/<backup_id>.sql.zip
```
3. Creating a task for Workers
**Stage 3**. Request from the Worker (agent) to `api.sodew.ai` to obtain a new task and receiving it.
The task is assigned to the Worker.
**Stage 4**. Processing of the `api.sodew.ai` response by the Worker (agent) and creating a task for the `kube.sh` script.
A task file `<task_id>.task` is created in the `workerTasksPath` folder with the following content:
* `action` – action
* `files_url` – URL to the files archive
* `database_url` – URL to the database archive
* `domain` – domain (optional)
* `status` – new (task status: new)
**Stage 5**. Launching the `kube.sh` task handler via CRON, searching for new tasks and starting execution of the found new task.
**Stage 6**. Validation of the task parameters. Checking the availability of backup files. Downloading the backup files. Creating the site. (Detailed: [Transfer](#transfer))
**Stage 7**. Monitoring of the task execution status by the Worker (agent) and sending reports to `api.sodew.ai`.
**Stage 8**. Receiving task execution reports from the Worker (agent).
**Stage 9**. Updating the task execution status on `api.sodew.ai` by the plugin.
## Workflow 1. Cloning a website via the WordPress plugin.
```mermaid
flowchart TB;
subgraph SW[Server-worker]
WO(Worker-observer<br>CRON)
WA(Worker-agent)
TP[[workerTasksPath/task_id.task<br>action<br>files_url<br>database_url<br>status=new]]
CS[Сopy of user's website]
end
subgraph API[api.sodew.ai]
AT(task_id<br>files_url<br>database_url)
end
subgraph US[User site]
WP(Plugin WP<br>action<br>transfer_id<br>backup_id<br>base_url)
end
WP -->|1. create task<br>check status| AT
WA -->|2. get new task<br>send statuses| AT
WA -->|3. save new task| TP
WO -->|4. executing tasks| TP
WO -->|5. load files| US
WO -->|6. create site| CS
```
## Workflow 2. Cloning websites to SODEW servers via the WordPress plugin.
```mermaid
flowchart LR;
US[[User sites...]]
SW[[Server-workers...]]
API(api.sodew.ai)
US -->|tasks...| API -->|tasks...| SW
```
## Workflow 3. Detailed description of the process.
```mermaid
sequenceDiagram
alt task
Wordpress plugin ->> api.sodew.ai: new task
loop every 30sec
Wordpress plugin ->> api.sodew.ai: how status task?
activate api.sodew.ai
api.sodew.ai ->> Wordpress plugin: status task is ...
deactivate api.sodew.ai
end
end
loop every 30sec
Worker-Agent ->> api.sodew.ai: receiving new tasks
activate api.sodew.ai
api.sodew.ai ->> Worker-Agent: <task>
deactivate api.sodew.ai
activate Worker-Agent
Worker-Agent ->> workerTasksPath: saving a new task <task>
deactivate Worker-Agent
end
loop every 15sec
Worker-Agent ->> workerTasksPath: reading task statuses
activate Worker-Agent
Worker-Agent ->> api.sodew.ai: sending task statuses
deactivate Worker-Agent
end
loop every 1min
Worker-Observer ->> workerTasksPath: get new task
Note over Worker-Observer: Execute task...
end
```
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
readonly appVersion="7.1.553"
readonly appName="KUBE"
readonly appPath="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
readonly appFile="$(basename -- "$0")"
readonly appDate="$(date +%Y-%m-%d)"
readonly appTime="$(date +%H-%M-%S)"
. "$appPath/libs/app.sh"
appBootstrap
function appDev() {
printRow
printFill 30 "◤◢" "$fontYellow" "◤$fontReset"
printFill 30 "◤◢" "$fontYellow" "◤$fontReset"
}
appRouter "$@"
exit $?
+132
View File
@@ -0,0 +1,132 @@
readonly EX_OK=0
readonly EX_GENERAL=1
readonly EX_USAGE=2
readonly EX_CONFIG=3
readonly EX_DEPENDENCY=4
readonly EX_PERMISSION=5
readonly EX_NOT_FOUND=6
readonly EX_COMMAND_FAILED=10
readonly EX_K8S=20
readonly EX_DB=30
readonly EX_BACKUP=40
# ERR_MSG=""
# ERR_CODE=0
# err_set() {
# ERR_CODE="$1"
# ERR_MSG="$2"
# return "$ERR_CODE"
# }
# err_clear() {
# ERR_CODE=0
# ERR_MSG=""
# }
# m_a_riadbStatus() {
# err_clear
# local out err
# if ! run out appError kubectl get pods -n mariadb; then
# err_set 30 "Cannot get MariaDB pods: $err"
# return $ERR_CODE
# fi
# log_table "$out"
# }
# Print an error message to stderr.
# $1 (message): error message text.
function appError() {
printf '%s\n' "$*" >&2
}
# Print a fatal error message and exit the script.
# [$1] (code): optional numeric exit code (defaults to 1); if omitted, $1 is the message.
# $1 (message): error message text (all remaining args when code is provided).
function appFailure() {
local code=1
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
code="$1"
shift || true
fi
printf '%b\n' "\033[0;91mCrash: $*\033[0m"
exit "$code"
}
# Source a shell file, exiting with failure if the file does not exist.
# $1 (file): path to the shell file to load.
function appLoadFile() {
local file="$1"
[[ -f "$file" ]] || appFailure 3 "File not found: $file"
. "$file"
}
# Validate all required configuration variables and abort on any missing or malformed value.
function appCheckConfig() {
local value
local -a values=('hostName' 'hostIp' 'hostUuid' 'hostSalt' 'k3sNamespace' 'redisKube' 'mariadbKube' 'olsKube' 'postfixKube' 'workerKube' 'redisFileUsersAcl' 'postfixIp' 'postfixHost' 'postfixPostmaster' 'postfixDkimSelector' 'workerApiUrl' 'workerName' 'backupType' 'backupFirstDir')
for value in "${values[@]}"; do
[[ -n "${!value-}" ]] || appFailure 4 "$value: not specified"
done
values=('appAssetsPath' 'appDataPath' 'basePath' 'olsVhostsPath' 'domainsList' 'k3sCmd' 'redisPath' 'mariadbMasterPath' 'mariadbSlavePath' 'olsPath' 'olsAdminPath' 'olsPhpIniPath' 'olsLogsPath' 'olsConfigFile' 'olsPrivatePath' 'olsPublicPath' 'olsVhostsConfigPath' 'postfixPath' 'postfixDkimPath' 'workerPath' 'workerAgentPath' 'workerTasksPath' 'workerFilesPath' 'logPath' 'logFile' 'backupDailyPath' 'backupArchivePath' 'storagePath' 'rsyncPath' 'ftpPath' 'sshPath')
for value in "${values[@]}"; do
[[ "${!value-}" == /* ]] || appFailure 4 "$value: a variable must begin with /"
[[ "${!value-}" != */ ]] || appFailure 4 "$value: a variable cannot end in /"
done
values=('backupParallelJobs' 'k3sReadyRetries' 'k3sReadySleep' 'workerApiGettingPause' 'workerApiSendingPause' 'backupDailyKeep' 'backupArchiveInterval' 'storageDailyKeep' 'storageArchiveKeep' 'olsQuotaBlockLimit' 'olsQuotaInodeLimit')
for value in "${values[@]}"; do
[[ "${!value-}" =~ ^[0-9]+$ ]] && (( ${!value} >= 1 )) || appFailure 4 "$value: incorrect number value"
done
if [[ "$backupDailySuffix" == "$backupArchiveSuffix" ]]; then
appFailure 4 "backupDailySuffix = backupArchiveSuffix"
fi
}
# Initialize the application by loading core libraries, config, modules, and commands.
function appBootstrap() {
local file
[[ "$EUID" -eq 0 ]] || appFailure 2 "You must run this script as root"
# Core
for file in \
"$appPath/libs/main.sh" \
"$appPath/libs/print.sh" \
"$appPath/libs/file.sh"
do
appLoadFile "$file"
done
# Config
appLoadFile "$appPath/config.sh"
appCheckConfig
# Modules
for file in "$appPath/libs/modules/"*.sh; do
appLoadFile "$file"
done
# Commands
for file in "$appPath/libs/commands/"*.sh; do
appLoadFile "$file"
done
}
# Dispatch execution to the appropriate router based on the APP_MODE environment variable.
function appRouter() {
local mode="${APP_MODE:-cmd}"
case "$mode" in
api) apiRouter "$@" ;;
cmd|*) cmdRouter "$@" ;;
esac
}
+383
View File
@@ -0,0 +1,383 @@
backupLabel="[Backup]"
# Creates archive-based backups for all OpenLiteSpeed virtual hosts.
# [$1] (path): destination directory.
# [$2] (type): archive type: zip or tar.
function cmdBackupSitesArchive() {
local path
path=$(backupPathGenerate "$1")
local type="${2:-$backupType}"
local error vhostList total
run vhostList error openlitespeedConfigVhostList || { printDanger "Failed get list of vhosts: $error"; return 1; }
total=$(grep -c . <<< "$vhostList")
printSection "Config"
printDotText "Target" "all ($total)"
printDotText "Type" "$type"
printDotText "Path" "$path"
printSection "Executing"
local domain label num i=0 lockFile tmpDir
maxJobs="${backupParallelJobs:-1}"
tmpDir=$(mktemp -d) || { printDanger "Failed to create temp directory"; return 1; }
lockFile="$tmpDir/.lock"
while read -r domain; do
((i++))
num=$(printf "%0${#total}d" "$i")
label="$num: $fontBlue$domain$fontReset"
while (( $(jobs -rp | wc -l) >= maxJobs )); do
wait -n 2>/dev/null || true
done
(
local jobError
if runError jobError backupSite "$domain" "$path" "$type"; then
{ flock 9; printDotText "$label" "$labelDone"; } 9>>"$lockFile"
else
touch "$tmpDir/$i"
{ flock 9; printDotText "$label" "$labelFail"; printDanger "$jobError"; } 9>>"$lockFile"
fi
) &
done < <(awk 'NF' <<< "$vhostList")
wait
local failed=0 f
for f in "$tmpDir"/[0-9]*; do
[[ -f "$f" ]] || break
((failed++))
done
rm -rf "$tmpDir"
printSection "Summary"
printDotText "Total" "$fontBlue$total$fontReset"
printDotText "Successful" "$fontGreen$((total-failed))$fontReset"
printDotText "Failed" "$fontRed$failed$fontReset"
}
# Creates rsync-based backups for all sites; first daily backup is full, later ones use hard links.
# [$1] (path): destination directory.
function cmdBackupSitesRsync() {
local path
path=$(backupPathGenerate "$1")
printSection "Config"
printDotText "Target" "all"
printDotText "Path" "$path"
local error vhostList
run vhostList error openlitespeedConfigVhostList || { printDanger "Failed get list of vhosts: $error"; return 1; }
printText "Files..."
if [[ "$path" == *"$backupFirstDir" ]]; then
runError error rsync -a --mkpath -- "$olsVhostsPath/" "$path" || printDanger "$error"
else
runError error rsync -a --link-dest="$backupDailyPath/$backupFirstDir" -- "$olsVhostsPath/" "$path" || printDanger "$error"
fi
printText "Databases..."
while read -r domain; do
runError error backupSiteDatabase "$domain" "$path/$domain.sql.tar.gz" || printDanger "$error"
done < <(awk 'NF' <<< "$vhostList")
printText "Configs..."
while read -r domain; do
runError error cp -p -- "$olsVhostsConfigPath/$domain.conf" "$path/$domain.conf" || printDanger "$error"
done < <(awk 'NF' <<< "$vhostList")
}
# Runs a backup for all sites using the configured or given backup type.
# [$1] (path): destination directory.
# [$2] (type): backup type: zip, tar, or rsync.
function cmdBackupSites() {
local path="$1"
path=$(backupPathGenerate "$path")
local type="${2:-$backupType}"
case "$type" in
zip|tar)
cmdBackupSitesArchive "$path" "$type" || return 1
;;
rsync)
cmdBackupSitesRsync "$path" || return 1
;;
*)
printSection "Config"
printDanger "Unknown backup type: $type"
return 1
;;
esac
}
function cmdBackupArchiveDispatch() {
printSection "Config"
[[ -n "$backupArchivePath" ]] || { printDotText "Path" "$labelNull"; return 1; }
printDotText "Path" "$backupArchivePath"
[[ -n "$backupArchiveInterval" ]] || { printDotText "Period" "$labelNull"; return 1; }
printDotText "Period" "$backupArchiveInterval"
[[ -n "$backupArchiveDirPattern" ]] || { printDotText "Pattern" "$labelNull"; return 1; }
printDotText "Pattern" "$backupArchiveDirPattern"
mkdir -p -- "$backupArchivePath" || { printDanger "Failed to create archive path"; return 1; }
local dirList archiveList dailyList error
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
printSection "Directories found"
local archiveCount archiveRemoveCount i=0 num label dirDate dirDays archiveRemoveList=""
archiveCount=$(grep -c . <<< "$archiveList" || true)
if [[ "$archiveCount" -gt 0 ]]; then
while read -r dir; do
((++i))
num=$(printf "%0${#archiveCount}d" "$i")
label="$num: $fontBlue$dir$fontReset"
if (( i == archiveCount )); then
printDotText "$label" "${fontGreen}save$fontReset"
elif (( i == archiveCount - 1 )); then
dirDate="${dir%$backupArchiveSuffix}"
dirDays=$(( $(timeDiff "$dirDate" "$appDate") / 86400 ))
if (( dirDays >= backupArchiveInterval )); then
printDotText "$label" "${fontRed}delete$fontReset"
archiveRemoveList+=$'\n'"$dir"
else
printDotText "$label" "${fontGreen}save$fontReset"
fi
else
printDotText "$label" "${fontRed}delete$fontReset"
archiveRemoveList+=$'\n'"$dir"
fi
done < <(awk 'NF' <<< "$archiveList")
archiveRemoveCount=$(grep -c . <<< "$archiveRemoveList" || true)
if [[ "$archiveRemoveCount" -gt 0 ]]; then
printSection "Deleting Directories"
while read -r dir; do
label="$backupArchivePath/$dir"
if [[ -n "$dir" ]]; then
if rm -rf -- "$backupArchivePath/$dir" &>/dev/null; then
printDotText "$label" "$labelDone"
else
printDotText "$label" "$labelFail"
fi
fi
done < <(awk 'NF' <<< "$archiveRemoveList")
fi
fi
# Promote oldest daily (excluding today) to archive
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
archiveCount=$(grep -c . <<< "$archiveList" || true)
if (( archiveCount < 2 )); then
local oldestDaily
oldestDaily=$(head -1 <<< "$dailyList" || true)
[[ -n "$oldestDaily" ]] || return 0
[[ "$oldestDaily" != "$appDate" ]] || return 0
printSection "Promote Directories"
mv -- "$backupDailyPath/$oldestDaily" "$backupArchivePath/${oldestDaily}$backupArchiveSuffix" || {
printDotText "$backupDailyPath/$oldestDaily" "$labelFail"
printDanger "Failed to promote daily to archive: $oldestDaily"
return 1
}
printDotText "$backupDailyPath/$oldestDaily" "$labelDone"
fi
}
function cmdBackupDailyDispatch() {
printSection "Config"
[[ -n "$backupDailyPath" ]] || { printDotText "Path" "$labelNull"; return 1; }
printDotText "Path" "$backupDailyPath"
[[ -n "$backupDailyKeep" ]] || { printDotText "Keep" "$labelNull"; return 1; }
printDotText "Keep" "$backupDailyKeep"
[[ -n "$backupDailyDirPattern" ]] || { printDotText "Pattern" "$labelNull"; return 1; }
printDotText "Pattern" "$backupDailyDirPattern"
mkdir -p -- "$backupDailyPath" || { printDanger "Failed to create archive path"; return 1; }
local dirList dailyList error
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
dailyList=$(printf '%s\n' "$dirList" | grep -v "$appDate" | grep -E -- "$backupDailyDirPattern" | sort || true)
printSection "Directories found"
local dailyCount dailyRemoveCount i=0 num label dailyRemoveList=""
dailyCount=$(grep -c . <<< "$dailyList" || true)
if [[ "$dailyCount" -gt 0 ]]; then
while read -r dir; do
((++i))
num=$(printf "%0${#dailyCount}d" "$i")
label="$num: $fontBlue$dir$fontReset"
if [[ "$dir" == "$appDate" ]]; then
printDotText "$label" "${fontGray}skipped$fontReset"
elif (( dailyCount - backupDailyKeep >= i )); then
printDotText "$label" "${fontRed}delete$fontReset"
dailyRemoveList+=$'\n'"$dir"
else
printDotText "$label" "${fontGreen}save$fontReset"
fi
done < <(awk 'NF' <<< "$dailyList")
dailyRemoveCount=$(grep -c . <<< "$dailyRemoveList" || true)
if [[ "$dailyRemoveCount" -gt 0 ]]; then
printSection "Deleting Directories"
while read -r dir; do
label="$backupDailyPath/$dir"
if [[ -n "$dir" ]]; then
if rm -rf -- "$backupDailyPath/$dir" &>/dev/null; then
printDotText "$label" "$labelDone"
else
printDotText "$label" "$labelFail"
fi
fi
done < <(awk 'NF' <<< "$dailyRemoveList")
fi
fi
}
function cmdBackupDispatch() {
local second=0 interval=0
printText "$fontMagenta[Step 1 Processing of archive backups on external storage]$fontReset"
cmdStorageBackupArchiveDispatch
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
interval="$seconds"
printRow
printText "$fontMagenta[Step 2 Processing of daily backups on external storage]$fontReset"
cmdStorageBackupDailyDispatch
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
interval="$seconds"
printRow
printText "$fontMagenta[Step 3 Processing of archive backups on host (SKIP...)]$fontReset"
# cmdBackupArchiveDispatch
# seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
# printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
# interval="$seconds"
printRow
printText "$fontMagenta[Step 4 Processing of daily backups on host]$fontReset"
cmdBackupDailyDispatch
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
interval="$seconds"
printRow
printText "$fontMagenta[Step 5 Creating a full daily backup]$fontReset"
cmdBackupSites
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
interval="$seconds"
printRow
printText "$fontMagenta[Step 6 Synchronization with external storage (daily backups)]$fontReset"
cmdStorageBackupDailySyncLast
seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
interval="$seconds"
printRow
printText "$fontMagenta[Step 7 Synchronization with external storage (archive backups) (SKIP...)]$fontReset"
# cmdStorageBackupArchiveSyncLast
# seconds=$(timeDiff "$appDate ${appTime//-/:}" "$(date +%Y-%m-%d) $(date +%H:%M:%S)")
# printDotText "Complete" "$(date +%Y-%m-%d) $(date +%H:%M:%S) $fontBlue$((seconds-interval))s$fontReset"
}
# Runs a backup for a single site or all sites.
# [$1] (target): site domain or "all".
# [$2] (path): destination directory.
# [$3] (type): backup type.
function cmdBackupRun() {
local target="$1"
[[ -n "$target" ]] || { printRow; printDanger "Target not specified"; return 1; }
local path="$2"
path=$(backupPathGenerate "$path")
local type="${3:-$backupType}"
if [[ "$target" == "all" ]]; then
cmdBackupSites "$path" "$type" || return 1
else
printSection "Config"
printDotText "Target" "$target"
printDotText "Type" "$type"
printDotText "Path" "$path"
printSection "Executing"
local label error
if runError error backupSite "$target" "$path" "$type"; then
printDotText "$target" "$labelDone"
else
printDotText "$target" "$labelFail"
printDanger "$error"
fi
fi
}
function cmdBackupList() {
printRow
local dirList archiveList dailyList backupList backupCount error
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
backupList=$(printf '%s\n' "$archiveList" "$dailyList" | awk 'NF' | sort)
backupCount=$(grep -c . <<< "$backupList" || true)
local i=0 num dir label
while read -r dir; do
((++i))
num=$(printf "%0${#backupCount}d" "$i")
label="$num: $fontBlue$dir$fontReset"
if listContains "$dir" "$archiveList"; then
printDotText "$label" "${fontGreen}archive$fontReset"
elif listContains "$dir" "$dailyList"; then
printDotText "$label" "${fontGreen}daily$fontReset"
else
printDotText "$label" "$labelUnknown"
fi
done < <(awk 'NF' <<< "$backupList")
}
function cmdBackupSize() {
local dirList file size total=0 archiveList dailyList
printSection "Archive: $backupArchivePath"
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
while IFS= read -r file; do
size=$(pathSize "$backupArchivePath/$file" "gb")
printDotText "$file" "$size Gb"
(( total += size ))
done <<< "$archiveList"
printSection "Daily: $backupDailyPath"
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
while IFS= read -r file; do
size=$(pathSize "$backupDailyPath/$file" "gb")
printDotText "$file" "$size Gb"
(( total += size ))
done <<< "$dailyList"
printRow
printDotText "total" "$total Gb"
}
+336
View File
@@ -0,0 +1,336 @@
function cmdHelpPrint() {
printf "%b" "\n$fontYellow $1$fontReset\n$2\n"
}
function cmdHelpK3S() {
local title="-k|--k3s <action>"
local desc="
create - create all resources in k3s
remove - remove all resources in k3s
info - detail about a k3s
status - status about a k3s
top - top pod
res [<resource>] - get resource info (all|pod|event|pv|pvc|deploy|ds|rs|sts|svc|ing|ip|secret|cm|job|cj|ep|no|ns|cs|netpol)"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpMariaDB() {
local title="-m|--mariadb <action>"
local desc="
install - install MariaDB in k3s
update - update MariaDB in k3s
uninstall - uninstall MariaDB from k3s
info - detail about a MariaDB
status - status about a MariaDB
replica - replica rebuild
database - database list
user - user list
dump [all|<database>] [<file>] - dump specified database or all databases from Master
dump_slave [<file>] - full dump from Slave"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpRedis() {
local title="-r|--redis <action>"
local desc="
install - install Redis in k3s
update - update Redis in k3s
uninstall - uninstall Redis from k3s
info - detail about a Redis
status - status about a Redis
user - user list
flush - flush current DB
pass - update default (root) pass"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpOpenLiteSpeed() {
local title="-o|--ols <action>"
local desc="
install - install OpenLiteSpeed in k3s
update - update OpenLiteSpeed in k3s
uninstall - uninstall OpenLiteSpeed from k3s
info - detail about a OpenLiteSpeed
list <option> - vhost list (all|up|down)
up - unpause vhost
down - pause vhost
alias - set aliases for domain
restart - restart OLS (graceful restart)
php_kill all|<domain> - kill lsphp for domain or all domains
white_list - WebAdmin white list update
allow_list - WebAdmin allow list update
alias_list all|<domain> - get aliases for domain or all domains
rebuild all|<domain> - rebuild owner and permission files
config - check config $olsConfigFile"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpPostfix() {
local title="-p|--postfix <action>"
local desc="
install - install Postfix in k3s
update - update Postfix in k3s
uninstall - uninstall Postfix from k3s
info - detail about a Postfix
status - status about a Postfix
user - user list (SASL)
dkim [<domain>] - autocreate and display DKIM key for DNS record or all domains !!!"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpWorker() {
local title="-w|--worker <action>"
local desc="
install - install Worker in k3s
update - update Worker in k3s
uninstall - uninstall Worker from k3s
list - task list
down - stop receiving new tasks from the API (pause)
up - start receiving new tasks from the API (unpause)
task <file> [<domain>] - Execute task !!!!!"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpMetric() {
local title="--metric <action>"
local desc="
install - install Metric in k3s
update - update Metric in k3s
uninstall - remove Metric from k3s
restart - restart !!!!"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpSite() {
local title="-s|--site <action>"
local desc="
add <domain> [<pathF>] [<pathD>] - add site (pathF: source files | pathD: source database)
add_wp|wp <domain> [<pathF>] [<pathD>] - add site on Wordpress
remove <domain> [-f|--force] - site full remove (-f|--force - forced mode)
copy <domain> <domainNew> - create copy of site
rename <domain> <domainNew> - rename of site
info <domain> - view site info and settings
status <domain> - check and verify site settings
rebuild <domain> - rebuild config for domain (in MariaDB, Redis, OLS, ...)
rebuild_wp <domain> - rewrite config connection to internal services in bootstrap.php
reset_pass all|<domain> - reset ALL passwords for domain or all domains
reset_auth all|<domain> - reset ALL authentication settings for domain or all domains
dump <domain> [<file>] - dump database of site"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpWP() {
local title="--wp <action>"
local desc="
user <domain> - user list
pass <domain> <user> <pass> - user password set
salt all|<domain> - shuffle salts
check all|<domain> - check core and plugins
exec all|<domain> <command> - command exec"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpSftp() {
local title="--sftp <action>"
local desc="
enable <domain> - enable sftp access
disable <domain> - disable sftp access
reset_pass <domain> - reset pass
user - list of users with SFTP access (group: $sftpAccessGroup)
support - adding support for SFTP access (group: $sftpAccessGroup)"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpCron() {
local title="--cron <action>"
local desc="
add - add jobs to cron
remove - remove jobs from cron
list - task list"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpBackup() {
local title="--backup <action>"
local desc="
run all|<domain> [<path>] [<type>] - backup
path: path to save backup, default: autogenerate
type: type backup (zip|tar|archive|rsync), default: $backupType
list - list backups on local
size - list of backup sizes"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpRestore() {
local title="--restore <domain> $fontRed[NO TESTED!]$fornReset"
local desc="
run <domain> [<option>] - manual site restore
<empty> - manual site restore
list - display a list of available markers for restore
last - automatic site restore from the last backup
full - automatic site restore from the last FULL backup
auto - automatic site restore from the last FULL backup or the last backup
N - automatic site restore from the last backup #N (N: 1+)"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpStorage() {
local title="--storage [option]"
local desc="
list - list backups on external storage
compare - comparison table
size - list of backup sizes
sync <type> - synchronization with external storage (archive|daily)
remove - remove backups on external storage"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpScript() {
local title="--script <action>"
local desc="
backup - creating a backup all sites and then synchronizing with external storage
backup-long-term - creating long-term backups
backup-clean - cleanup of old backups on the current host and on external storage
mariadb-dump - creating a backup of all databases in MariaDB
worker-observer - launching the task observer
metric-kube - send kube metrics to API (Grafana)
metric-sites - send sites metrics to API
diag-report-ai-short - send diag short report to AI
diag-report-ai-full - send diag full report to AI"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpTest() {
local title="--test <action> $fontRed[NO TESTED!]$fornReset"
local desc="
mariadb - test MariaDB replica
redis - test Redis cluster
site - test create default site
wordpress - test create Wordpress site"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpMalware() {
local title="--malware <action>"
local desc="
plugin - check plugins (files) in all vhosts
user - check users Wordpress in all vhosts
file all|<domain> - check files"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpShell() {
local title="--shell [cli]"
local desc="
opening the shell or cli (if any) in the pods"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpLog() {
local title="--log [parameters]"
local desc="
opening the logs in the pods. Standard kubectl parameters for logs can be added, for example:
-f: logs should be streamed
--previous: print the logs for the previous instance of the container in a pod if it exists"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpDescribe() {
local title="--describe <option>"
local desc="
pod - describe pods
node - describe node"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpDelete() {
local title="--delete"
local desc="
delete pods"
cmdHelpPrint "$title" "$desc"
}
function cmdHelpInstall() {
local title="--install"
local desc="
install full infrastructure (apk, update ipset/iptables, install k3s, apply yaml...)"
cmdHelpPrint "$title" "$desc"
}
function cmdHelp() {
local title="$appName"
local desc="
Usage: $0 [arguments...]"
cmdHelpPrint "$title" "$desc"
printDotFix 20 "$fontBlue -k|--k3s" "Commands for k3s"
printDotFix 20 "$fontBlue -m|--mariadb" "Commands for MariaDB"
printDotFix 20 "$fontBlue -r|--redis" "Commands for Redis"
printDotFix 20 "$fontBlue -o|--ols" "Commands for Openlitespeed"
printDotFix 20 "$fontBlue -p|--postfix" "Commands for Postfix"
printDotFix 20 "$fontBlue -w|--worker" "Commands for Worker (agent)"
printDotFix 20 "$fontBlue -s|--site" "Commands for Sites"
printDotFix 20 "$fontBlue --metric" "Commands for Metrics"
printDotFix 20 "$fontBlue --wp" "Commands for Wordpress"
printDotFix 20 "$fontBlue --sftp" "Commands for SFTP"
printDotFix 20 "$fontBlue --cron" "Commands for Cron"
printDotFix 20 "$fontBlue --shell" "Shell or cli (if any) in pods"
printDotFix 20 "$fontBlue --log" "Log of pods"
printDotFix 20 "$fontBlue --describe" "Describe of pods"
printDotFix 20 "$fontBlue --delete" "Delete pods"
printDotFix 20 "$fontBlue --backup" "Create backup of sites"
printDotFix 20 "$fontBlue --restore" "Restore sites from backups"
printDotFix 20 "$fontBlue --storage" "Copy backups to storage"
printDotFix 20 "$fontBlue --script" "Execute scripts"
printDotFix 20 "$fontBlue --install" "Install full infrastructure"
printDotFix 20 "$fontBlue --test" "Testing infrastructure"
printDotFix 20 "$fontBlue --malware" "Malware search"
printDotFix 20 "$fontBlue --help" "This help"
printRow
cmdHelpK3S
cmdHelpMariaDB
cmdHelpRedis
cmdHelpOpenLiteSpeed
cmdHelpPostfix
cmdHelpWorker
cmdHelpSite
cmdHelpMetric
cmdHelpWP
cmdHelpSftp
cmdHelpCron
cmdHelpShell
cmdHelpLog
cmdHelpDescribe
cmdHelpDelete
cmdHelpBackup
cmdHelpRestore
cmdHelpStorage
cmdHelpScript
cmdHelpInstall
cmdHelpTest
cmdHelpMalware
}
+378
View File
@@ -0,0 +1,378 @@
k3sLabel="[K3S]"
# Interactively lists pods and stores the selected pod name in the given variable.
# $1 (varname): name of the variable to store the selected pod name.
function cmdK3sPodSelect() {
local -n __pod="$1"
printRow
local podList error total
run podList error k3sPodListStatus || { printDanger "k3sPodListStatus: $error"; return 1; }
[[ -n "$podList" ]] || { printRow printDanger "Pods not found"; return 1; }
total=$(grep -c . <<< "$podList")
local i=0 line name status color num
while IFS= read -r line; do
[[ -z "$line" ]] && continue
((i++))
num=$(printf "%${#total}d" "$i")
name="${line%%|*}"
status="${line#*|}"
color="$fontYellow"
[[ "$status" == "Running" ]] && color="$fontGreen"
[[ "$status" == "NotReady" ]] && color="$fontRed"
[[ "$status" == "Terminating" ]] && color="$fontRed"
printDotText "$num: $fontBlue$name$fontReset" "$color$status$fontReset"
done <<< "$podList"
printRow
local input item selected
read -r -p "Specify the pod number: " input
printRow
[[ -z "$input" ]] && input=0
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid pod number"; return 1; }
item=$((10#$input))
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$podList")
[[ -n "$selected" ]] || { printDanger "Unknown pod number"; return 1; }
__pod="${selected%%|*}"
}
# Interactively opens a shell or CLI inside a selected pod.
# [$1] (cli): pass "cli" to open the native CLI (mariadb/redis-cli) instead of a shell.
function cmdShell() {
printTitle " Shell $@"
local cli="$1"
local pod
cmdK3sPodSelect pod || return 1
local resource resourceEx
resource=$(printf '%s' "$pod" | sed -E 's/-([0-9a-f]{8,}-[a-z0-9]+|[a-z0-9]{5}|[0-9]+)$//')
resourceEx=$(printf '%s' "$pod" | sed -E 's/-([-a-z0-9]+)$//')
local container=(-c "$resource")
local cmd=(bash)
local cmdLog=(bash)
case "$resourceEx" in
"$redisKube"|"$metricKube"|debug)
cmd=(sh)
cmdLog=(sh)
;;
esac
if [[ "$cli" == "cli" ]]; then
case "$resource" in
"$mariadbMasterKube"|"$mariadbSlaveKube")
cmd=(mariadb -uroot -p"$mariadbRootPass")
cmdLog=(mariadb -uroot -p"********")
;;
"$redisKube")
if [[ -z "$redisRootPass" ]]; then
cmd=(redis-cli)
cmdLog=(redis-cli)
else
cmd=(redis-cli --no-auth-warning -a "$redisRootPass")
cmdLog=(redis-cli --no-auth-warning -a"********")
fi
;;
"$redisSentinelKube")
if [[ -z "$redisRootPass" ]]; then
cmd=(redis-cli -p 26379)
cmdLog=(redis-cli -p 26379)
else
cmd=(redis-cli --no-auth-warning -p 26379 -a "$redisRootPass")
cmdLog=(redis-cli --no-auth-warning -p 26379 -a"********")
fi
;;
esac
fi
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace exec -it pod/$pod ${container[*]} -- ${cmdLog[*]}$fontReset"
printRow
k3sRun exec -it "pod/$pod" "${container[@]}" -- "${cmd[@]}"
}
# Interactively selects a pod and streams its logs.
# [$@] (...): extra arguments passed to kubectl logs (e.g. -f, --tail=100).
function cmdLog() {
printTitle " Log $@"
local pod
cmdK3sPodSelect pod || return 1
local resource
resource=$(printf '%s' "$pod" | sed -E 's/-([0-9a-f]{8,}-[a-z0-9]+|[a-z0-9]{5}|[0-9]+)$//')
local container=(-c "$resource")
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace logs pod/$pod ${container[*]} $*$fontReset"
printRow
k3sRun logs "pod/$pod" "${container[@]}" "$@"
}
# Interactively selects a pod and describes it; also supports describing a node.
# [$1] (target): pod (default) or node.
function cmdDescribe() {
local target="${1:-pod}"
shift || true
printTitle " Describe $@"
case "$target" in
pod)
local pod
cmdK3sPodSelect pod || return 1
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace describe pod/$pod $*$fontReset"
printRow
k3sRun describe "pod/$pod" "$@"
k3sRun describe "pod/$pod" "$@"
;;
node)
k3sRun describe node "$@"
;;
*)
printRow
printWarning "Unsupported or empty command: $target"
cmdHelpDescribe
;;
esac
}
# Interactively selects and deletes a pod.
function cmdDelete() {
printTitle " Delete $@ $fontRed[DANGER]$fontReset"
local pod
cmdK3sPodSelect pod || return 1
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace delete pod/$pod $*$fontReset"
printRow
k3sRun delete "pod/$pod" "$@"
}
# Lists k3s resources; interactively prompts for type if not provided.
# [$1] (resource): resource type abbreviation (pod, deploy, svc, etc.).
function cmdK3sResourceList() {
local resource="$1"
if [[ ! "$resource" =~ ^(all|pod|event|pv|pvc|deploy|ds|rs|sts|svc|ing|ip|secret|cm|job|cj|ep|no|ns|cs|netpol)$ ]]; then
local resList="
all|Get all resources
pod|Pod
event|Event
pv|PersistentVolume
pvc|PersistentVolumeClaim
deploy|Deployment
ds|DaemonSet
rs|ReplicaSet
sts|StatefulSet
svc|Service
ing|Ingress
ip|IPAddress
secret|Secret
cm|ConfigMap
job|Job
cj|CronJob
ep|Endpoint
no|Node
ns|Namespace
cs|ComponentStatuses
netpol|NetworkPolicies"
printRow
local i=0 line code info num
total=$(grep -c . <<< "$resList")
while IFS= read -r line; do
[[ -n "$line" ]] || continue
((i++))
num=$(printf "%${#total}d" "$i")
code="${line%%|*}"
info="${line#*|}"
printDotFix 20 "$num: $fontBlue$code$fontReset" "$info"
done <<< "$resList"
printRow
local input item selected
read -r -p "Specify the type number [0]: " input
printRow
[[ -z "$input" ]] && input=0
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid type number"; return 1; }
item=$((10#$input))
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$resList")
[[ -n "$selected" ]] || { printDanger "Unknown type number"; return 1; }
resource="${selected%%|*}"
fi
run output error k3sRun get "$resource" || { printDanger "$error"; return 1; }
printf '%s\n' "$output"
}
# Installs k3s on the server.
function cmdK3sInstall() {
printInfo "$k3sLabel Install..."
curl -sfL https://get.k3s.io | sh - || return 1
systemctl enable --now k3s
}
# Uninstalls k3s from the server.
function cmdK3sUninstall() {
/usr/local/bin/k3s-uninstall.sh
}
# Adds a namespace to Kubernetes if it does not already exist.
# $1 (namespace): namespace name.
function cmdK3sNamespaceAdd() {
local output error
run output error "$k3sCmd" kubectl get ns -o jsonpath="{range .items[*]}{.metadata.name}{'\n'}{end}" || { printDanger "$k3sLabel $error"; return 1; }
if ! grep -qF -- "$k3sNamespace" <<< "$output"; then
run output error "$k3sCmd" kubectl create ns "$k3sNamespace" || { printDanger "$k3sLabel $error"; return 1; }
fi
}
# Deletes all resources in the current namespace.
function cmdK3sResourceClean() {
printWarning "$k3sLabel Namespace: $k3sNamespace | Wiping..."
k3sRun delete all --all --ignore-not-found --wait=false --timeout=30s --request-timeout=60s || true
k3sRun delete cm,secret,ingress,networkpolicy,svc,pvc,job,cronjob --all --ignore-not-found --wait=false --timeout=30s --request-timeout=60s || true
mapfile -t pvs < <(
"$k3sCmd" kubectl get pv -o jsonpath='{range .items[?(@.spec.claimRef.namespace=="'"$k3sNamespace"'")]}{.metadata.name}{"\n"}{end}' 2>/dev/null
)
local pv
for pv in "${pvs[@]}"; do
[[ -n "$pv" ]] || continue
"$k3sCmd" kubectl patch pv "$pv" --type=merge -p '{"metadata":{"finalizers":[]}}' || true
"$k3sCmd" kubectl delete pv "$pv" --wait=false --timeout=15s || true
done
printSuccess "$k3sLabel Namespace: $k3sNamespace | Wiped"
}
# Validates, applies a YAML file, then waits for new pods to become ready.
# $1 (file): path to the YAML configuration file.
function cmdK3sYamlApplyEx() {
local file="$1" output error
printSection "Applying YAML file"
printDotText "file" "$file"
run output error k3sYamlCheck "$file" || {
printDotText "applying" "$labelFail"
printDanger "Error checking YAML: ${error:-$output}"
return 1
}
printDotText "checking" "$labelDone"
k3sPodsSnapshot podList || {
printDotText "applying" "$labelFail"
printDanger "Failed get list of pods"
return 1
}
runError error k3sYamlApply "$file" || {
printDotText "applying" "$labelFail"
printDanger "Error applied YAML: ${error:-$output}"
return 1
}
printDotText "applying" "$labelDone"
k3sWaitNewPodsReady podList || return 1
}
# Displays pods, services, ingress, and kube-system services info.
function cmdK3sInfo() {
local output error line
printSection "k3s"
if ! run output error k3sRun version; then
printDanger "$error";
else
while IFS= read -r line; do
printDotText "${line%% Version:*}" "${line##*: }"
done < <(awk 'NF' <<< "$output")
fi
if ! run output error "$k3sCmd" kubectl get nodes --no-headers; then
printDanger "$error"
else
local name status roles age version
while IFS='|' read -r name status roles age version; do
printSection "$name"
if [[ "$status" == "Ready" ]]; then
printDotText "Status" "$fontGreen$status$fontReset"
else
printDotText "Status" "$fontRed$status$fontReset"
fi
printDotText "Roles" "$roles"
printDotText "Age" "$age"
printDotText "Version" "$version"
done < <(awk 'NF{print $1 "|" $2 "|" $3 "|" $4 "|" $5}' <<< "$output")
fi
}
# Displays pods, services, ingress, kube-system services, and non-running pod bugs.
function cmdK3sNodesStatus() {
printSection "Pods"
if run output error k3sRun get pods -o wide; then
printText "$output"
else
printDanger "$error"
fi
printSection "Services"
if run output error k3sRun get svc -o wide; then
printText "$output"
else
printDanger "$error"
fi
printSection "Ingress"
if run output error k3sRun get ing; then
printText "$output"
else
printDanger "$error"
fi
printSection "Services (kube-system)"
if run output error "$k3sCmd" kubectl -n kube-system get svc; then
printText "$output"
else
printDanger "$error"
fi
printSection "Bugs..."
local output error
if run output error kubectl get pods -A \
--field-selector=status.phase!=Running,status.phase!=Pending \
-o custom-columns='NS:.metadata.namespace,POD:.metadata.name,PHASE:.status.phase,REASON:.status.reason,NODE:.spec.nodeName'; then
printText "$output"
else
printDanger "$error"
fi
printText "
PHASE | REASON
------------------
Failed | !=0 Process crashed
Failed | Error Process ended with an error
Failed | OOMKilled Ran out of memory
Failed | Evicted Kubelet evicted the pod (disk/memory pressure)
Completed/Succeeded - Not error if Job/migration/init task"
}
# Displays resource usage (CPU/memory) for all pods in the namespace.
function cmdK3sTopPod() {
local output error
run output error k3sRun top pod || { printDanger "$error"; return 1; }
printRow
printText "$output"
}
+268
View File
@@ -0,0 +1,268 @@
function cmdMalwareUserList() {
printRow
local output error databaseList
if ! run output error mariadbDatabaseListGet; then
printDotText "Databases" "$labelUnknown"
printDanger "$error"
return
fi
mapfile -t databaseList < <(awk 'NF' <<< "$output")
printDotText "Databases" "${#databaseList[@]}"
(( ${#databaseList[@]} > 0 )) || return
printRow
local inList
inList=$(printf "'%s'," "${databaseList[@]}")
inList="${inList%,}"
local sql
sql=$(cat << 'EOF'
SET SESSION group_concat_max_len = 1000000;
SELECT GROUP_CONCAT(CONCAT(
"SELECT CONVERT('", t.table_schema, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS db_name,",
" ID,",
" CONVERT(user_login USING utf8mb4) COLLATE utf8mb4_unicode_ci AS user_login,",
" user_registered,",
" CONVERT('", t.table_name, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS table_name",
" FROM `", t.table_schema, "`.`", t.table_name, "`",
" WHERE user_login LIKE 'adm\\_%' OR user_login LIKE 'admin\\_%' OR user_login LIKE 'administrator\\_%' OR user_login LIKE 'backup\\_%'"
) SEPARATOR ' UNION ALL ') INTO @sql
FROM information_schema.tables t
WHERE t.table_schema IN (__IN_LIST__)
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='user_login')
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='ID')
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='user_registered');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
EOF
)
sql="${sql/__IN_LIST__/$inList}"
mariadbMasterRootQuery "$sql"
}
function cmdMalwareOptionsList() {
printRow
local output error databaseList
if ! run output error mariadbDatabaseListGet; then
printDotText "Databases" "$labelUnknown"
printDanger "$error"
return
fi
mapfile -t databaseList < <(awk 'NF' <<< "$output")
printDotText "Databases" "${#databaseList[@]}"
(( ${#databaseList[@]} > 0 )) || return
printRow
local inList
inList=$(printf "'%s'," "${databaseList[@]}")
inList="${inList%,}"
local sql
sql=$(cat << 'EOF'
SET SESSION group_concat_max_len = 1000000;
SELECT GROUP_CONCAT(CONCAT(
"SELECT CONVERT('", t.table_schema, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS db_name,",
" option_id,",
" CONVERT(option_name USING utf8mb4) COLLATE utf8mb4_unicode_ci AS option_name,",
" CONVERT(autoload USING utf8mb4) COLLATE utf8mb4_unicode_ci AS autoload,",
" CONVERT('", t.table_name, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS table_name",
" FROM `", t.table_schema, "`.`", t.table_name, "`",
" WHERE option_name LIKE 'sc\\_%'"
) SEPARATOR ' UNION ALL ') INTO @sql
FROM information_schema.tables t
WHERE t.table_schema IN (__IN_LIST__)
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_id')
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_name')
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='autoload');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
EOF
)
sql="${sql/__IN_LIST__/$inList}"
mariadbMasterRootQuery "$sql"
}
function cmdMalwareMuPluginList() {
local allowedFiles="
index.php
00-hosting-loader.php
load.php
hosting-wp-domain-rename.php
burst_rest_api_optimizer.php
elementor-safe-mode.php
mailoptin-customizer-optimizer.php
wgpwpp-cache.php
installatron_hide_status_test.php
"
run vhostsList error fileList "$olsVhostsPath" d || { printDanger "$error"; return 1; }
while read -r dir; do
local found=0 pluginList
# unknown
run itemList error fileList "$olsVhostsPath/$dir/www/wp-content/mu-plugins/" f || continue
while read -r item; do
if grep -qF '($i){static $a=null' "$olsVhostsPath/$dir/www/wp-content/mu-plugins/$item"; then
(( found++ )) || printSection "$dir"
printDotText "$item" "${fontRed}malware$fontReset"
elif ! listContains "$item" "$allowedFiles"; then
(( found++ )) || printSection "$dir"
printDotText "/wp-content/mu-plugins/$item" "$labelUnknown"
fi
done < <(awk 'NF' <<< "$itemList")
# wp2shell
pluginList=$(find "$olsVhostsPath/$dir/www/wp-content/plugins" -maxdepth 1 -type d -name 'wp2shell*' 2>/dev/null)
if [ -n "$pluginList" ]; then
while IFS= read -r item; do
(( found++ )) || printSection "$dir"
printDotText "${item#"$olsVhostsPath/$dir/www"}" "${fontRed}malware$fontReset"
done <<< "$pluginList"
fi
# wp-static-cache
pluginList=$(find "$olsVhostsPath/$dir/www/wp-content/plugins" -maxdepth 1 -type d -name 'wp-static-cache*' 2>/dev/null)
if [ -n "$pluginList" ]; then
while IFS= read -r item; do
(( found++ )) || printSection "$dir"
printDotText "${item#"$olsVhostsPath/$dir/www"}" "${fontRed}malware$fontReset"
done <<< "$pluginList"
fi
# other
for subdir in wp-content/mu-plugins wp-content/plugins; do
pluginList=$(find "$olsVhostsPath/$dir/www/$subdir" -maxdepth 1 -regextype posix-extended -regex '^.*[^0-9a-f][0-9a-f]{6,8}(\.php)?$' 2>/dev/null)
if [ -n "$pluginList" ]; then
while IFS= read -r item; do
(( found++ )) || printSection "$dir"
printDotText "${item#"$olsVhostsPath/$dir/www"}" "${fontYellow}warning$fontReset"
done <<< "$pluginList"
fi
done
done < <(awk 'NF' <<< "$vhostsList")
}
function cmdMalwareFilesCheck() {
local target="$1"
local vhostList error
printRow
if [[ -z "$target" ]]; then
printDanger "Target not specified";
elif ! run vhostList error openlitespeedConfigVhostList; then
printDanger "Cannot get vhost list: $error";
elif [[ "$target" == "all" ]]; then
local domain
for domain in $vhostList; do
mapfile -t diffArray < <(siteFilesCheck "$domain")
if [[ ${#diffArray[@]} -gt 0 ]]; then
printSection "$domain"
local line
for line in "${diffArray[@]}"; do
printText "$line"
done
fi
done
elif ! listContains "$target" "$vhostList"; then
printDanger "Unknown domain: $target";
else
mapfile -t diffArray < <(siteFilesCheck "$target")
if [[ ${#diffArray[@]} -gt 0 ]]; then
printDotText "$target" "$labelFail"
local line
for line in "${diffArray[@]}"; do
printText "$line"
done
else
printDotText "$target" "$labelDone"
fi
fi
}
function cmdMalwareOptionsTimestamps() {
printRow
local output error databaseList
if ! run output error mariadbDatabaseListGet; then
printDotText "Databases" "$labelUnknown"
printDanger "$error"
return
fi
mapfile -t databaseList < <(awk 'NF' <<< "$output")
printDotText "Databases" "${#databaseList[@]}"
(( ${#databaseList[@]} > 0 )) || return
printRow
local inList
inList=$(printf "'%s'," "${databaseList[@]}")
inList="${inList%,}"
local sql
sql=$(cat << 'EOF'
SET SESSION group_concat_max_len = 1000000;
SELECT GROUP_CONCAT(CONCAT(
"SELECT CONVERT('", t.table_schema, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS db_name,",
" option_id,",
" CONVERT(option_name USING utf8mb4) COLLATE utf8mb4_unicode_ci AS option_name,",
" CONVERT(option_value USING utf8mb4) COLLATE utf8mb4_unicode_ci AS option_value",
" FROM `", t.table_schema, "`.`", t.table_name, "`",
" WHERE option_name IN ('sc_last_fetch_ts','sc_last_rescan','sc_last_rpc')"
) SEPARATOR ' UNION ALL ') INTO @sql
FROM information_schema.tables t
WHERE t.table_schema IN (__IN_LIST__)
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_id')
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_name')
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_value');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
EOF
)
sql="${sql/__IN_LIST__/$inList}"
mariadbMasterRootQuery "$sql" | sort -t$'\t' -k4 -rn
}
function cmdMalwareOptionsSuspiciousNames() {
printRow
local output error databaseList
if ! run output error mariadbDatabaseListGet; then
printDotText "Databases" "$labelUnknown"
printDanger "$error"
return
fi
mapfile -t databaseList < <(awk 'NF' <<< "$output")
printDotText "Databases" "${#databaseList[@]}"
(( ${#databaseList[@]} > 0 )) || return
printRow
local inList
inList=$(printf "'%s'," "${databaseList[@]}")
inList="${inList%,}"
local sql
sql=$(cat << 'EOF'
SET SESSION group_concat_max_len = 1000000;
SELECT GROUP_CONCAT(CONCAT(
"SELECT CONVERT('", t.table_schema, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS db_name,",
" CONVERT('", t.table_name, "' USING utf8mb4) COLLATE utf8mb4_unicode_ci AS table_name,",
" option_id,",
" CONVERT(option_name USING utf8mb4) COLLATE utf8mb4_unicode_ci AS option_name",
" FROM `", t.table_schema, "`.`", t.table_name, "`",
" WHERE option_name LIKE 'sc\\_%' ESCAPE '\\\\'",
" OR option_name REGEXP '^[0-9a-f]{12}$'"
) SEPARATOR ' UNION ALL ') INTO @sql
FROM information_schema.tables t
WHERE t.table_schema IN (__IN_LIST__)
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_id')
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_name')
AND EXISTS (SELECT 1 FROM information_schema.columns c WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='option_value');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
EOF
)
sql="${sql/__IN_LIST__/$inList}"
mariadbMasterRootQuery "$sql" | sort -t$'\t' -k1,1 -k2,2 -k4,4
}
+439
View File
@@ -0,0 +1,439 @@
mariadbLabel="[MariaDB]"
# Prepares Kubernetes secrets for MariaDB.
function cmdMariadbPreparation() {
printInfo "$mariadbLabel Preparation..."
local error
runError error k3sRun delete secret "$mariadbKube-secret" --ignore-not-found || {
printDotText "preparation" "$labelFail"
printDanger "Delete old Secret: $error"
return 1
}
runError error k3sRun create secret generic "$mariadbKube-secret" --from-literal=root-password="$mariadbRootPass" --from-literal=replication-password="$mariadbRootPass" || {
printDotText "preparation" "$labelFail"
printDanger "Create new Secret: $error"
return 1
}
printDotText "preparation" "$labelDone"
}
# Renders the MariaDB Kubernetes YAML manifest via Helm.
function cmdMariadbYamlRender() {
local templateFile="templates/$mariadbKube.yaml"
printSection "Render YAML file | Helm"
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
printDanger "Template file not found"
return 1
}
local profileCpuFile="$appAssetsPath/k3s/profiles/cpu-$kubeCpuProfile.yaml"
printDotText "CPU profile" "$profileCpuFile"
[[ -f "$profileCpuFile" ]] || {
printDanger "CPU profile file not found"
return 1
}
local profileMemoryFile="$appAssetsPath/k3s/profiles/memory-$kubeMemoryProfile.yaml"
printDotText "Memory profile" "$profileMemoryFile"
[[ -f "$profileMemoryFile" ]] || {
printDanger "Memory profile file not found"
return 1
}
local varsFile
varsFile=$(mktemp "/tmp/$mariadbKube.vars.XXXXXX.yaml") || {
printDotText "render" "$labelFail"
printDanger "Create temp variables file"
return 1
}
printDotText "Variables" "$varsFile"
trap 'rm -f -- "$varsFile"' RETURN
cat > "$varsFile" <<EOF
mariadbHelm: true
namespace: $k3sNamespace
mariadb: $mariadbKube
mariadbMaster: $mariadbMasterKube
mariadbSlave: $mariadbSlaveKube
mariadbMasterPath: $mariadbMasterPath
mariadbSlavePath: $mariadbSlavePath
EOF
printDotText "Result" "$mariadbYaml"
mkdir -p -- "$(dirname -- "$mariadbYaml")" || return 1
fileBackup "$mariadbYaml"
helm template stack "$appAssetsPath/k3s" -f "$varsFile" -f "$profileCpuFile" -f "$profileMemoryFile" --show-only "$templateFile" > "$mariadbYaml" || {
printDotText "render" "$labelFail"
printDanger "Render failed"
return 1
}
printDotText "render" "$labelDone"
}
# Waits until both MariaDB master and slave respond to SQL queries.
function cmdMariadbWaitMasterSlave() {
local error step attempts=15
for ((step=1; step<=attempts; step++)); do
runError error mariadbMasterRootQuery "SELECT 1;" && break
if [[ "$step" -ne "$attempts" ]]; then
printWarning "$mariadbLabel Master node | Waiting..."
sleep 4
else
printDanger "$mariadbLabel Master node | Not responding"
return 1
fi
done
for ((step=1; step<=attempts; step++)); do
runError error mariadbSlaveRootQuery "SELECT 1;" && break
if [[ "$step" -ne "$attempts" ]]; then
printWarning "$mariadbLabel Slave node | Waiting..."
sleep 4
else
printDanger "$mariadbLabel Slave node | Not responding"
return 1
fi
done
}
# Rebuilds MariaDB replication from master to slave.
# [$1] (masterPassword): replication password (defaults to $mariadbRootPass).
function cmdMariadbReplicaRebuild() {
local masterPassword="${1:-$mariadbRootPass}"
printInfo "$mariadbLabel Replica rebuild..."
local output error
runError error mariadbMasterRootQuery "SELECT 1;" || {
printDanger "$mariadbLabel Master node | Not responding: $error"
return 1
}
runError error mariadbSlaveRootQuery "SELECT 1;" || {
printDanger "$mariadbLabel Slave node | Not responding: $error"
return 1
}
runError error mariadbMasterRootQuery "CREATE USER IF NOT EXISTS 'replication_user'@'%' IDENTIFIED BY '$masterPassword'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'replication_user'@'%'; ALTER USER 'replication_user'@'%' IDENTIFIED BY '$masterPassword';" || {
printDanger "$mariadbLabel Master node | Recreated replication user: $error"
return 1
}
printInfo "$mariadbLabel Master node | Recreated replication user"
local dumpFile="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_replica_rebuild.sql"
printInfo "$mariadbLabel Master node | Exporting full dump..."
mariadbMasterRootExport all "$dumpFile" || {
printDanger "$mariadbLabel Master node | Export failed"
return 1
}
printInfo "$mariadbLabel Master node | Exported: $dumpFile"
local masterFile masterPos
masterFile=$(grep -m1 -oE "MASTER_LOG_FILE='[^']+'" "$dumpFile" | sed "s/MASTER_LOG_FILE='//;s/'//")
masterPos=$(grep -m1 -oE "MASTER_LOG_POS=[0-9]+" "$dumpFile" | sed 's/MASTER_LOG_POS=//')
[[ -n "$masterFile" && -n "$masterPos" ]] || {
printDanger "$mariadbLabel Master node | Cannot parse MASTER_LOG_FILE/MASTER_LOG_POS from dump"
return 1
}
printInfo "$mariadbLabel Master node | Binlog: $masterFile:$masterPos"
printInfo "$mariadbLabel Slave node | Stopping replication..."
runError error mariadbSlaveRootQuery "STOP SLAVE; RESET SLAVE ALL;" || {
printDanger "$mariadbLabel Slave node | Stop/reset failed: $error"
return 1
}
printInfo "$mariadbLabel Slave node | Importing full dump..."
runShell output error k3sRun exec -i pod/"$mariadbSlaveKube"-0 -c "$mariadbSlaveKube" -- mariadb -u "root" -p"$mariadbRootPass" "<" "$dumpFile" ">" /dev/null || {
printDanger "$mariadbLabel Slave node | Import failed: ${error:-$output}"
return 1
}
printInfo "$mariadbLabel Slave node | Import completed"
runError error mariadbSlaveRootQuery "CHANGE MASTER TO MASTER_HOST='${mariadbMasterKube}', MASTER_PORT=3306, MASTER_USER='replication_user', MASTER_PASSWORD='${masterPassword}', MASTER_LOG_FILE='${masterFile}', MASTER_LOG_POS=${masterPos}; START SLAVE;" || {
printDanger "$mariadbLabel Slave node | Configure replication failed: $error"
return 1
}
printInfo "$mariadbLabel Slave node | Replication configured"
run output error mariadbSlaveRootQuery "SHOW SLAVE STATUS\G" showColumn || {
printDanger "$mariadbLabel Slave node | Status: $error"
return 1
}
local ioRunning sqlRunning secondsBehind lastError
ioRunning=$(printf '%s\n' "$output" | awk -F': ' '/^ *Slave_IO_Running:/{print $2}' | tr -d '[:space:]')
sqlRunning=$(printf '%s\n' "$output" | awk -F': ' '/^ *Slave_SQL_Running:/{print $2}' | tr -d '[:space:]')
secondsBehind=$(printf '%s\n' "$output" | awk -F': ' '/^ *Seconds_Behind_Master:/{print $2}' | tr -d '[:space:]')
lastError=$(printf '%s\n' "$output" | awk -F': ' '/^ *Last_Error:/{print substr($0, index($0,$2))}')
if [[ "$ioRunning" != "Yes" || "$sqlRunning" != "Yes" ]]; then
printWarning "$mariadbLabel Slave node | IO:${ioRunning:-?} | SQL:${sqlRunning:-?}"
[[ -n "$lastError" ]] && printWarning "$mariadbLabel Slave node | Last error: $lastError"
return 1
fi
printSuccess "$mariadbLabel Replica rebuild completed | Delay ${secondsBehind:-0}s"
}
# Updates MariaDB Kubernetes resources (limits, config, etc.) without re-initialization.
function cmdMariadbUpdate() {
cmdMariadbYamlRender || return 1
cmdK3sYamlApplyEx "$mariadbYaml" || return 1
}
# Installs MariaDB into Kubernetes and initializes replication.
function cmdMariadbInstall() {
cmdMariadbPreparation || return 1
cmdMariadbYamlRender || return 1
cmdK3sYamlApplyEx "$mariadbYaml" || return 1
cmdMariadbWaitMasterSlave || return 1
cmdMariadbReplicaRebuild || return 1
}
# Uninstalls MariaDB Kubernetes resources.
function cmdMariadbUninstall() {
"$k3sCmd" kubectl delete -f "$mariadbYaml"
}
# Checks MariaDB root password, replication health, database/user count, and total data size.
function cmdMariadbInfo() {
local password
password=$(mariadbRootPassSecretGet)
if [[ "$password" != "$mariadbRootPass" ]]; then
printRow
printDotText "Root password" "$labelFail"
printDanger "Use mariadbRootPassSecretSave"
return 1
fi
local podList output error pod phase
# Master
if ! run podList error k3sPodListStatus "$mariadbMasterKube"; then
printDanger "Get pods (master): $error"
elif [[ -z "$podList" ]]; then
printDanger "Pods (master) not found"
else
while IFS='|' read -r pod phase; do
printSection "$pod"
if [[ "$phase" != "Running" ]]; then
printDotText "Phase" "$fontRed$phase$fontReset"
else
printDotText "Phase" "$fontGreen$phase$fontReset"
if ! run output error mariadbMasterRootQuery "SELECT VERSION();"; then
printDotText "MariaDB status" "$fontRed$labelFail$fontReset"
printDanger "$error"
continue
fi
printDotText "MariaDB status" "$labelRunning"
printDotText "MariaDB version" "$output"
local masterStatus file position activeConnections
if ! run masterStatus error mariadbMasterRootQuery "SHOW MASTER STATUS\G;" "showColumn"; then
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
printDanger "$error"
continue
fi
file=$(printf '%s\n' "$masterStatus" | awk '/^ *File:/{print $2}')
if [[ -z "$file" ]]; then
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
printDanger "Params 'File' not found"
continue
fi
position=$(printf '%s\n' "$masterStatus" | awk '/^ *Position:/{print $2}')
if [[ -z "$position" ]]; then
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
printDanger "Params 'Position' not found"
continue
fi
if ! run output error mariadbMasterRootQuery "SHOW STATUS LIKE 'Threads_connected';"; then
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
printDanger "$error"
continue
fi
printDotText "Replica role" "${fontGreen}master$fontReset"
activeConnections=$(printf '%s\n' "$output" | awk '{print $2}')
if [[ "$activeConnections" -ge 100 || "$activeConnections" -eq 1 ]]; then
printDotText "Active connections" "$fontYellow$activeConnections$fontReset"
else
printDotText "Active connections" "$fontGreen$activeConnections$fontReset"
fi
fi
done < <(awk 'NF' <<< "$podList")
fi
# Slave
if ! run podList error k3sPodListStatus "$mariadbSlaveKube"; then
printDanger "Get pods (slave): $error"
elif [[ -z "$podList" ]]; then
printDanger "Pods (slave) not found"
else
while IFS='|' read -r pod phase; do
printSection "$pod"
if [[ "$phase" != "Running" ]]; then
printDotText "Phase" "$fontRed$phase$fontReset"
else
printDotText "Phase" "$fontGreen$phase$fontReset"
if ! run output error mariadbSlaveRootQuery "SELECT VERSION();"; then
printDotText "MariaDB status" "$fontRed$labelFail$fontReset"
printDanger "$error"
continue
fi
printDotText "MariaDB status" "$labelRunning"
printDotText "MariaDB version" "$output"
local slaveStatus slaveIoRunning slaveSqlRunning lastError secondsBehindMaster
if ! run slaveStatus error mariadbSlaveRootQuery "SHOW SLAVE STATUS\G;" "showColumn"; then
printDotText "Replica role" "$fontRed$labelUnknown$fontReset"
printDanger "$error"
continue
fi
slaveIoRunning=$(printf '%s\n' "$slaveStatus" | awk '/^ *Slave_IO_Running:/{print $2}')
if [[ "$slaveIoRunning" != "Yes" ]]; then
printDotText "Slave IO Running" "$fontRed$slaveIoRunning$fontReset"
continue
fi
slaveSqlRunning=$(printf '%s\n' "$slaveStatus" | awk '/^ *Slave_SQL_Running:/{print $2}')
if [[ "$slaveSqlRunning" != "Yes" ]]; then
printDotText "Slave SQL Running" "$fontRed$slaveSqlRunning$fontReset"
continue
fi
lastError=$(printf '%s\n' "$slaveStatus" | awk '/^ *Last_Error:/{$1=""; sub(/^[ \t]+/,""); print}')
if [[ -n "$lastError" ]]; then
printDotText "Slave SQL Running" "$fontRed$slaveSqlRunning$fontReset"
printDanger "Last error: $lastError"
continue
fi
printDotText "Replica role" "${fontGreen}slave$fontReset"
secondsBehindMaster=$(printf '%s\n' "$slaveStatus" | awk '/^ *Seconds_Behind_Master:/{print $2}')
if [[ "$secondsBehindMaster" -ne 0 ]]; then
printDotText "Replication lags" "$fontYellow${secondsBehindMaster}s$fontReset"
else
printDotText "Replication lags" "${fontGreen}0s$fontReset"
fi
fi
done < <(awk 'NF' <<< "$podList")
fi
printSection "MariaDB"
local databaseList userList dataSize
if run output error mariadbDatabaseListGet; then
mapfile -t databaseList < <(awk 'NF' <<< "$output")
printDotText "Databases" "${#databaseList[@]}"
else
printDotText "Databases" "$labelUnknown"
printDanger "$error"
fi
if run output error mariadbUserListGet; then
mapfile -t userList < <(awk 'NF' <<< "$output")
printDotText "Users" "${#userList[@]}"
else
printDotText "Users" "$labelUnknown"
printDanger "$error"
fi
if ! run dataSize error mariadbMasterRootQuery "SELECT ROUND(COALESCE(SUM(DATA_LENGTH + INDEX_LENGTH), 0) / 1024 / 1024 / 1024, 2) AS t FROM information_schema.TABLES WHERE TABLE_TYPE = 'BASE TABLE';"; then
printDotText "Size" "$labelUnknown"
printDanger "$error"
else
printDotText "Size" "$dataSize Gb"
fi
}
# Shows MariaDB master and slave replication status.
function cmdMariadbStatus() {
local output error
printSection "$mariadbMasterKube"-0
if ! run output error mariadbMasterRootQuery "SHOW MASTER STATUS;"; then
printDanger "$error"
else
printText "$output"
fi
printSection "$mariadbSlaveKube"-0
if ! run output error mariadbSlaveRootQuery "SHOW SLAVE STATUS\G;" showColumn; then
printDanger "$error"
else
printText "$output"
fi
}
# Lists all MariaDB databases.
function cmdMariadbDatabase() {
local output error
printRow
if ! run output error mariadbDatabaseListGet; then
printDanger "$error"
elif [[ -z "$output" ]]; then
printText "$labelNull"
else
printText "$output"
fi
}
# Lists all MariaDB users.
function cmdMariadbUser() {
local output error
printRow
if ! run output error mariadbUserListGet; then
printDanger "$error"
elif [[ -z "$output" ]]; then
printText "$labelNull"
else
printText "$output"
fi
}
# Exports a full dump from the MariaDB master node.
# [$@] (...): arguments passed to mariadbMasterRootExport (e.g. database name, file).
function cmdMariadbMasterDump() {
local output error
printRow
if ! run output error mariadbMasterRootExport "$@"; then
printDanger "$error"
else
printText "$output"
fi
}
# Exports a full dump from the MariaDB slave node.
# [$@] (...): arguments passed to mariadbSlaveRootExport (e.g. database name, file).
function cmdMariadbSlaveDump() {
local output error
printRow
if ! run output error mariadbSlaveRootExport "$@"; then
printDanger "$error"
else
printText "$output"
fi
}
+68
View File
@@ -0,0 +1,68 @@
metricLabel="[Metric]"
# Copies vmagent config file to the configured metric path.
function cmdMetricPreparation() {
printSection "Preparation..."
mkdir -p -- "$(dirname -- "$metricVmagentPath")" || return 1
cp -f -- "$appAssetsPath/metric/vmagent.yml" "$metricVmagentPath/vmagent.yml"
printDotText "preparation" "$labelDone"
}
# Renders the Metric Kubernetes YAML manifest via Helm.
function cmdMetricYamlRender() {
local templateFile="templates/$metricKube.yaml"
printSection "Render YAML file | Helm"
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
printDanger "Template file not found"
return 1
}
local varsFile
varsFile=$(mktemp "/tmp/$metricKube.vars.XXXXXX.yaml") || {
printDotText "render" "$labelFail"
printDanger "Create temp variables file"
return 1
}
printDotText "Variables" "$varsFile"
trap 'rm -f -- "$varsFile"' RETURN
cat > "$varsFile" <<EOF
metricHelm: true
namespace: $k3sNamespace
metric: $metricKube
metricApiUrl: $metricApiUrl
metricPromPath: $metricPromPath
metricVmagentPath: $metricVmagentPath
EOF
printDotText "Result" "$metricYaml"
mkdir -p -- "$(dirname -- "$metricYaml")" || return 1
fileBackup "$metricYaml"
helm template stack "$appAssetsPath/k3s" -f "$varsFile" --show-only "$templateFile" > "$metricYaml" || {
printDotText "render" "$labelFail"
printDanger "Render failed"
return 1
}
printDotText "render" "$labelDone"
}
function cmdMetricxUpdate() {
cmdMetricYamlRender || return 1
cmdK3sYamlApplyEx "$metricYaml" || return 1
}
# Installs Metric components into Kubernetes.
function cmdMetricInstall() {
cmdMetricPreparation || return 1
cmdMetricYamlRender || return 1
cmdK3sYamlApplyEx "$metricYaml" || return 1
}
# Uninstalls Metric Kubernetes resources.
function cmdMetricUninstall() {
"$k3sCmd" kubectl delete -f "$metricYaml"
}
@@ -0,0 +1,498 @@
openlitespeedLabel="[OpenLiteSpeed]"
# Renders the OpenLiteSpeed Kubernetes YAML manifest via Helm.
function cmdOpenlitespeedYamlRender() {
local templateFile="templates/$olsKube.yaml"
printSection "Render YAML file | Helm"
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
printDanger "Template file not found"
return 1
}
local profileCpuFile="$appAssetsPath/k3s/profiles/cpu-$kubeCpuProfile.yaml"
printDotText "CPU profile" "$profileCpuFile"
[[ -f "$profileCpuFile" ]] || {
printDanger "CPU profile file not found"
return 1
}
local profileMemoryFile="$appAssetsPath/k3s/profiles/memory-$kubeMemoryProfile.yaml"
printDotText "Memory profile" "$profileMemoryFile"
[[ -f "$profileMemoryFile" ]] || {
printDanger "Memory profile file not found"
return 1
}
local adminWhiteList=() adminWhiteStr
for i in "${!olsAdminWhiteList[@]}"; do
adminWhiteList[$i]="\"${olsAdminWhiteList[$i]}\""
done
adminWhiteStr=$(IFS=','; printf '%s\n' "${adminWhiteList[*]}")
local varsFile
varsFile=$(mktemp "/tmp/$olsKube.vars.XXXXXX.yaml") || {
printDotText "render" "$labelFail"
printDanger "Create temp variables file"
return 1
}
printDotText "Variables" "$varsFile"
trap 'rm -f -- "$varsFile"' RETURN
cat > "$varsFile" <<EOF
openlitespeedHelm: true
namespace: $k3sNamespace
openlitespeed: $olsKube
olsPodVhostsPath: $olsPodVhostsPath
olsPodPrivatePath: $olsPodPrivatePath
olsPodPublicPath: $olsPodPublicPath
olsVhostsPath: $olsVhostsPath
olsPrivatePath: $olsPrivatePath
olsPublicPath: $olsPublicPath
olsConfigPath: $olsConfigPath
olsPhpIniPath: $olsPhpIniPath
olsLogsPath: $olsLogsPath
olsAdminPath: $olsAdminPath
olsAdminWhiteList: $adminWhiteStr
EOF
printDotText "Result" "$olsYaml"
mkdir -p -- "$(dirname -- "$olsYaml")" || return 1
fileBackup "$olsYaml"
helm template stack "$appAssetsPath/k3s" -f "$varsFile" -f "$profileCpuFile" -f "$profileMemoryFile" --show-only "$templateFile" > "$olsYaml" || {
printDotText "render" "$labelFail"
printDanger "Render failed"
return 1
}
printDotText "render" "$labelDone"
}
# Initializes OpenLiteSpeed after Kubernetes deployment: patches Traefik, sets admin credentials, PHP config, rules, and restarts.
function cmdOpenlitespeedInit() {
printSection "Initialization..."
local ug
ug="$(stat -c "%u:%g" "$olsConfigFile")"
# Patch svc traefik
runSilent "$k3sCmd" kubectl -n kube-system patch svc traefik -p '{"spec": {"externalTrafficPolicy": "Local"}}' || {
printDotText "Patch svc traefik" "$labelFail"
return 1
}
printDotText "Patch svc traefik" "$labelDone"
cmdOpenlitespeedWaitReady || return 1
# Updating Administrator Password
runSilent cmdOpenlitespeedAdminPassUpdate "$olsAdminPass" || {
printDotText "Updating admin password" "$labelFail"
return 1
}
printDotText "Updating admin password" "$labelDone"
# Adding redirect rules
mkdir -p "$olsConfigPath/rules"
cp -n "$appAssetsPath"/openlitespeed/rules/* "$olsConfigPath/rules/"
chown -R "$ug" "$olsConfigPath/rules"
chmod 750 -R "$olsConfigPath/rules"
printDotText "Adding redirect rules" "$labelDone"
# Adding PHP configs
local profileFile="$appAssetsPath/openlitespeed/profiles/memory-$kubeMemoryProfile.config"
local children max_memory_limit memory_limit max_execution_time post_max_size upload_max_filesize
children=$(configGet "$profileFile" "children")
max_memory_limit=$(configGet "$profileFile" "max_memory_limit")
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")
local phpIniFile="$olsPhpIniPath/00-ols-master.ini"
fileBackup "$phpIniFile"
cp -f -- "$appAssetsPath/openlitespeed/php/00-ols-master.ini" "$phpIniFile"
sed -i \
-e "s|{{children}}|$children|g" \
-e "s|{{max_memory_limit}}|$max_memory_limit|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" \
-- "$phpIniFile"
find "$olsPhpIniPath" -type f -exec chmod 644 {} +
printDotText "Adding PHP configs" "$labelDone"
# Path OLS Admin config
runSilent openlitespeedAdminAllowList || {
printDotText "Path OLS Admin config" "$labelFail"
return 1
}
printDotText "Path OLS Admin config" "$labelDone"
# Path OLS config
openlitespeedConfigRebuild || {
printDotText "Path OLS config" "$labelFail"
return 1
}
printDotText "Path OLS config" "$labelDone"
cmdOpenlitespeedRestart
cmdOpenlitespeedPhpKill all
}
# Updates OpenLiteSpeed Kubernetes resources (limits, replicas, etc.) without re-initialization.
function cmdOpenlitespeedUpdate() {
cmdOpenlitespeedYamlRender || return 1
cmdK3sYamlApplyEx "$olsYaml" || return 1
}
# Installs OpenLiteSpeed into Kubernetes and runs initialization.
function cmdOpenlitespeedInstall() {
cmdOpenlitespeedYamlRender || return 1
cmdK3sYamlApplyEx "$olsYaml" || return 1
cmdOpenlitespeedInit
}
# Uninstalls OpenLiteSpeed Kubernetes resources.
function cmdOpenlitespeedUninstall() {
"$k3sCmd" kubectl delete -f "$olsYaml"
}
# Waits until OpenLiteSpeed WebAdmin PHP is ready.
function cmdOpenlitespeedWaitReady() {
local tries=${k3sReadyRetries:-10}
local sleepSec=${k3sReadySleep:-2}
local i output error
for ((i=1; i<=tries; i++)); do
run output error openlitespeedExec /usr/local/lsws/admin/fcgi-bin/admin_php -v && return 0
printWarning "$openlitespeedLabel Waiting..."
sleep "$sleepSec"
done
printDanger "$openlitespeedLabel Not ready after ${tries} tries"
return 1
}
# Updates OpenLiteSpeed WebAdmin credentials.
# $1 (password): WebAdmin password.
# [$2] (user): WebAdmin username (default: admin).
function cmdOpenlitespeedAdminPassUpdate() {
local password="$1"
[[ -n "$password" ]] || { printDanger "$openlitespeedLabel Password not specified"; return 1; }
local user="${2:-admin}"
local encrypt output error
run encrypt error openlitespeedExec /usr/local/lsws/admin/fcgi-bin/admin_php -q /usr/local/lsws/admin/misc/htpasswd.php "$password" || {
printDotText "Get encrypt" "$labelFail"
printDanger "$error"
return 1
}
printDotText "Get encrypt" "$labelDone"
run output error openlitespeedExec bash -c "echo '$user:$encrypt' > /usr/local/lsws/admin/conf/htpasswd" || {
printDotText "Save data" "$labelFail"
printDanger "$error"
return 1
}
printDotText "Save data" "$labelDone"
# if admin... save to <hostname>.openlitespeed
}
# Restarts OpenLiteSpeed on all OLS pods.
function cmdOpenlitespeedRestart() {
local podList error
run podList error k3sPodListStatus "$olsKube" || { printDanger "Get pods: $error"; return 1; }
[[ -n "$podList" ]] || { printDanger "Pods not found"; return 1; }
local pod phase output
while IFS='|' read -r pod phase; do
printSection "$pod"
if [[ "$phase" != "Running" ]]; then
printDotText "Phase" "$fontRed$phase$fontReset"
else
printDotText "Phase" "$fontGreen$phase$fontReset"
if ! run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lswsctrl restart; then
printDotText "Restart" "$labelUnknown"
printDanger "$error"
elif [[ "$output" =~ "[OK]" ]]; then
printDotText "Restart" "$fontGreen$output$fontReset"
else
printDotText "Restart" "$fontRed$output$fontReset"
fi
fi
done < <(awk 'NF' <<< "$podList")
}
# Restarts PHP workers on all OLS pods.
function cmdOpenlitespeedPhpKill() {
local target="$1"
[[ -n "$target" ]] || { printRow; printDanger "Target not specified"; return 1; }
local podList error
run podList error k3sPodListStatus "$olsKube" || { printRow; printDanger "Get pods: $error"; return 1; }
[[ -n "$podList" ]] || { printRow; printDanger "Pods not found"; return 1; }
local uid=""
if [[ "$target" != "all" ]]; then
local vhostList
run vhostList error openlitespeedConfigVhostList || { printRow; printDanger "Cannot get vhost list: $error"; return 1; }
listContains "$target" "$vhostList" || { printRow; printDanger "Unknown domain: $target"; return 1; }
uid=$(domainToUid "$target")
[[ -n "$uid" ]] || { printDanger "Cannot resolve UID for: $target"; return 1; }
fi
local pod phase
while IFS='|' read -r pod phase; do
printSection "$pod"
if [[ "$phase" != "Running" ]]; then
printDotText "Phase" "$fontRed$phase$fontReset"
else
printDotText "Phase" "$fontGreen$phase$fontReset"
if [[ -n "$uid" ]]; then
runSilent openlitespeedPodExec "$pod" pkill -u "$uid" -x lsphp
else
runSilent openlitespeedPodExec "$pod" pkill -x lsphp
fi
printDotText "kill$fontBlue lsphp$fontReset processes for $target" "$labelDone"
fi
done < <(awk 'NF' <<< "$podList")
}
# Prints OpenLiteSpeed and PHP versions for each OLS pod.
function cmdOpenlitespeedInfo() {
local output error podList ver pid
if ! run podList error k3sPodListStatus "$olsKube"; then
printDanger "Get pods: $error"
elif [[ -z "$podList" ]]; then
printDanger "Pods not found"
else
while IFS='|' read -r pod phase; do
printSection "$pod"
if [[ "$phase" != "Running" ]]; then
printDotText "Phase" "$fontRed$phase$fontReset"
else
printDotText "Phase" "$fontGreen$phase$fontReset"
if ! run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lswsctrl status; then
printDotText "Status" "$labelUnknown"
printDanger "$error"
elif [[ "$output" =~ "running" ]]; then
printDotText "OpenLiteSpeed status" "$fontGreen$output$fontReset"
else
printDotText "OpenLiteSpeed status" "$fontRed$output$fontReset"
fi
if run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lshttpd -v; then
ver=$(awk 'NR==1{print $1, $2}' <<< "$output")
printDotText "OpenLiteSpeed version" "$ver"
else
printDotText "OpenLiteSpeed version" "$labelUnknown"
printDanger "$error"
fi
if run output error openlitespeedPodExec "$pod" php -r 'echo PHP_VERSION, "\n";'; then
printDotText "PHP version" "$output"
else
printDotText "PHP version" "$labelUnknown"
printDanger "$error"
fi
fi
done < <(awk 'NF' <<< "$podList")
fi
printSection "Hosts"
local vhostList vhostDown
if run output error openlitespeedConfigVhostList; then
mapfile -t vhostList < <(awk 'NF' <<< "$output")
printDotText "all" "${#vhostList[@]}"
else
printDotText "all" "$labelUnknown"
printDanger "$error"
fi
if run output error openlitespeedConfigVhostList "down"; then
mapfile -t vhostDownList < <(awk 'NF' <<< "$output")
printDotText "down" "${#vhostDownList[@]}"
else
printDotText "down" "$labelUnknown"
printDanger "$error"
fi
local count="$(find "$olsVhostsPath" -mindepth 1 -maxdepth 1 -type d | wc -l)"
printDotText "directories" "$fontGray$olsVhostsPath$fontReset $count"
}
# Marks a virtual host as suspended.
# $1 (domain): site domain name.
function cmdOpenlitespeedVhostDown() {
printRow
local error
if ! runError error openlitespeedVhostConfigDown "$@"; then
printDotText "VHost down" "$labelFail"
printDanger "$error"
else
printDotText "VHost down" "$labelPass"
cmdOpenlitespeedRestart
fi
}
# Marks a virtual host as active.
# $1 (domain): site domain name.
function cmdOpenlitespeedVhostUp() {
printRow
local error
if ! runError error openlitespeedVhostConfigUp "$@"; then
printDotText "VHost up" "$labelFail"
printDanger "$error"
else
printDotText "VHost up" "$labelPass"
cmdOpenlitespeedRestart
fi
}
# Lists virtual hosts with optional status filtering.
# [$1] (type): all (default) | up | down.
function cmdOpenlitespeedSiteList() {
printRow
local output error type="${1:-all}"
if ! run output error openlitespeedConfigVhostList "$type"; then
printDanger "$error"
else
printText "$output"
fi
}
# Updates the Traefik middleware allowlist for OpenLiteSpeed WebAdmin.
function cmdOpenlitespeedAdminWhiteList() {
printRow
local output error
run output error openlitespeedAdminWhiteList || { printDotText "Update" "$labelFail"; printDanger "$error"; return 1; }
printDotText "White list" "$output"
printDotText "Update" "$labelDone"
}
# Updates OpenLiteSpeed WebAdmin access control from current Traefik pod IPs.
function cmdOpenlitespeedAdminAllowList() {
printRow
local output error
run output error openlitespeedAdminAllowList || { printDotText "Update" "$labelFail"; printDanger "$error"; return 1; }
printDotText "Allow list: ${output:-$labelNull}"
printDotText "Update" "$labelDone"
cmdOpenlitespeedRestart
}
# Sets aliases for an OpenLiteSpeed virtual host.
# $1 (domain): primary site domain name.
# $@ (...): alias domain names.
function cmdOpenlitespeedVhostAliasSet() {
local domain
domain=$(domainPrepare "$1")
printRow
domainCheck "$domain" || return 1
local vhostList aliasList output error
if ! run vhostList error openlitespeedConfigVhostList; then
appError "Error retrieving vhost list: $error"
return 1
elif ! listContains "$domain" "$vhostList"; then
appError "Domain not exists in OpenLiteSpeed config: $domain"
return 1
fi
run output error openlitespeedVhostSet "$@" || {
printDotText "Set" "$labelFail"
printDanger "$error"
return 1
}
printDotText "Alias" "${output:-$labelNull}"
printDotText "Set" "$labelDone"
cmdOpenlitespeedRestart
}
# Lists aliases for a domain or all domains.
# [$1] (target): domain name, or "all" to list all.
function cmdOpenlitespeedAliasList() {
printRow
local output error domain target="$1"
if [[ "$target" == all ]]; then
if ! run output error openlitespeedConfigAliasList; then
printDanger "$error"
elif [[ -z "$output" ]]; then
printText "$labelNull"
else
printText "$output"
fi
else
domain=$(domainPrepare "$target")
if ! run output error openlitespeedConfigVhostAliasList "$domain"; then
printDanger "$error"
elif [[ -z "$output" ]]; then
printText "$labelNull"
else
printText "$output"
fi
fi
}
# Tests the OpenLiteSpeed configuration inside the container.
function cmdOpenlitespeedConfigCheck() {
printRow
local output error
run output error openlitespeedExec sh -lc "/usr/local/lsws/bin/openlitespeed -t 2>/dev/null || true"
if grep -qi 'configuration failed!' <<< "$output"; then
printDotText "Check config" "$labelFail"
printDanger "$output"
else
printDotText "Check config" "$labelPass"
fi
}
function cmdOpenlitespeedVhostRebuild() {
local target="$1"
local vhostList error
printRow
if [[ -z "$target" ]]; then
printDanger "Target not specified";
elif ! run vhostList error openlitespeedConfigVhostList; then
printDanger "Cannot get vhost list: $error";
elif [[ "$target" == "all" ]]; then
local domain
for domain in $vhostList; do
if ! runError error openlitespeedVhostRebuild "$domain"; then
printDotText "$domain" "$labelFail"
printDanger "$error"
else
printDotText "$domain" "$labelDone"
fi
done
elif ! listContains "$target" "$vhostList"; then
printDanger "Unknown domain: $target";
elif ! runError error openlitespeedVhostRebuild "$target"; then
printDotText "$target" "$labelFail"
printDanger "$error"
else
printDotText "$target" "$labelDone"
fi
}
+276
View File
@@ -0,0 +1,276 @@
postfixLabel="[Postfix]"
# Generates a self-signed TLS certificate for Postfix if one does not already exist.
function cmdPostfixPreparation() {
printSection "Preparation..."
if [[ ! -f "$postfixTlsCrtFile" || ! -f "$postfixTlsKeyFile" ]]; then
local crtPath; crtPath=$(dirname "$postfixTlsCrtFile")
local tlsCnfFile="$crtPath/openssl.cnf"
local cn="${postfixHost:-$postfixDomain}"
local sanList="DNS:${cn}"
[[ -n "$postfixDomain" ]] && sanList="${sanList},DNS:${postfixDomain}"
mkdir -p "$crtPath" || {
printDotText "preparation" "$labelFail"
printDanger "Failed to create folder for certificate";
return 1;
}
cat >"$tlsCnfFile" <<EOF
[req]
distinguished_name = dn
x509_extensions = v3_req
prompt = no
[dn]
CN = ${cn}
[v3_req]
subjectAltName = ${sanList}
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
EOF
openssl req -x509 -nodes -newkey rsa:2048 -days 365 -keyout "$postfixTlsKeyFile" -out "$postfixTlsCrtFile" -config "$tlsCnfFile" || {
printDotText "preparation" "$labelFail"
printDanger "TLS gen failed";
return 1;
}
fi
printDotText "preparation" "$labelDone"
}
# Renders the Postfix Kubernetes YAML manifest via Helm.
function cmdPostfixYamlRender() {
local templateFile="templates/$postfixKube.yaml"
printSection "Render YAML file | Helm"
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
printDanger "Template file not found"
return 1
}
local val postfixTlsCrtB64 postfixTlsKeyB64
val=$(base64 -w0 "$postfixTlsCrtFile") || {
printDotText "render" "$labelFail"
printDanger "Base64 gen failed (1)"
return 1
}
postfixTlsCrtB64=$(sed 's/[&\\]/\\&/g' <<<"$val") || {
printDotText "render" "$labelFail"
printDanger "Base64 gen failed (2)"
return 1
}
val=$(base64 -w0 "$postfixTlsKeyFile") || {
printDotText "render" "$labelFail"
printDanger "Base64 gen failed (3)"
return 1
}
postfixTlsKeyB64=$(sed 's/[&\\]/\\&/g' <<<"$val") || {
printDotText "render" "$labelFail"
printDanger "Base64 gen failed (4)"
return 1
}
local varsFile
varsFile=$(mktemp "/tmp/$postfixKube.vars.XXXXXX.yaml") || {
printDotText "render" "$labelFail"
printDanger "Create temp variables file"
return 1
}
printDotText "Variables" "$varsFile"
trap 'rm -f -- "$varsFile"' RETURN
cat > "$varsFile" <<EOF
postfixHelm: true
namespace: $k3sNamespace
postfix: $postfixKube
postfixPath: $postfixPath
postfixTlsCrtB64: $postfixTlsCrtB64
postfixTlsKeyB64: $postfixTlsKeyB64
postfixHost: $postfixHost
postfixPostmaster: $postfixPostmaster
postfixDefaultRealm: $postfixDefaultRealm
postfixConfigPath: $postfixConfigPath
postfixDkimPath: $postfixDkimPath
postfixDkimSelector: $postfixDkimSelector
postfixDomainsFile: $postfixDomainsFile
postfixAliasesFile: $postfixAliasesFile
postfixSendersFile: $postfixSendersFile
EOF
printDotText "Result" "$postfixYaml"
mkdir -p -- "$(dirname -- "$postfixYaml")" || return 1
fileBackup "$postfixYaml"
helm template stack "$appAssetsPath/k3s" -f "$varsFile" --show-only "$templateFile" > "$postfixYaml" || {
printDotText "render" "$labelFail"
printDanger "Render failed"
return 1
}
printDotText "render" "$labelDone"
}
# Initializes Postfix after install: generates DKIM for postfixHost and sets sasldb2 ownership.
function cmdPostfixInit() {
printSection "Initialization..."
touch "$postfixConfigPath/$postfixDomainsFile" || return 1
touch "$postfixConfigPath/$postfixAliasesFile" || return 1
touch "$postfixConfigPath/$postfixSendersFile" || return 1
postfixDkimAdd "$postfixHost" || {
printDotText "Add DKIM for host" "$labelFail"
return 1
}
printDotText "Add DKIM for host" "$labelDone"
local error
if ! runError error postfixExec chown postfix:postfix /config/sasldb2; then
printDotText "Set owner /config/sasldb2" "$labelFail"
printDanger "$error"
return 1
fi
printDotText "Set owner /config/sasldb2" "$labelDone"
}
function cmdPostfixUpdate() {
cmdPostfixYamlRender || return 1
cmdK3sYamlApplyEx "$postfixYaml" || return 1
}
# Installs Postfix into Kubernetes.
function cmdPostfixInstall() {
cmdPostfixPreparation || return 1
cmdPostfixYamlRender || return 1
cmdK3sYamlApplyEx "$postfixYaml" || return 1
cmdPostfixInit || return 1
}
# Uninstalls Postfix Kubernetes resources.
function cmdPostfixUninstall() {
"$k3sCmd" kubectl delete -f "$postfixYaml"
}
function cmdPostfixUser() {
local output error
printRow
if ! run output error postfixUserList; then
printDanger "$error"
elif [[ -z "$output" ]]; then
printText "$labelNull"
else
printText "$output"
fi
}
# Prints the Postfix mail version.
function cmdPostfixInfo() {
local output error podList ver pid
if ! run podList error k3sPodListStatus "$postfixKube"; then
printDanger "Get pods: $error"
elif [[ -z "$podList" ]]; then
printDanger "Pods not found"
else
while IFS='|' read -r pod phase; do
printSection "$pod"
if [[ "$phase" != "Running" ]]; then
printDotText "Phase" "$fontRed$phase$fontReset"
else
printDotText "Phase" "$fontGreen$phase$fontReset"
if ! run output error postfixPodExec "$pod" postfix status; then
printDotText "Postfix status" "$fontRed$labelFail$fontReset"
printDanger "$error"
else
output="${output:-$error}"
if [[ $output == *"is running"* ]]; then
pid=${output##*PID: }
pid=${pid%%[^0-9]*}
printDotText "Postfix status" "$labelRunning"
printDotText "pid" "$pid"
else
printDotText "Postfix status" "$fontRed$output$fontReset"
fi
fi
if ! run output error postfixPodExec "$pod" postconf mail_version; then
printDotText "Postfix mail version" "$fontRed$labelFail$fontReset"
printDanger "$error"
else
ver=$(printf '%s' "$output" | cut -d '=' -f2 | tr -d '\r\n')
printDotText "Postfix mail version" "$ver"
fi
fi
done < <(awk 'NF' <<< "$podList")
fi
}
# Checks MTA host DNS records (A, SPF, PTR, DKIM) against configured values.
function cmdPostfixMtaDnsCheck() {
local label output error text
printSection "MTA DNS: $postfixHost"
# A record
if ! run output error dig +short A "$postfixHost" "$dnsResolver"; then
printDotText "A record" "$fontRed${output:-$error}$fontReset"
else
text=$(printf '%s' "$output" | paste -sd, - | sed 's/,/ \/ /g')
if grep -Fxq -- "$postfixIp" <<<"$output"; then
printDotText "A record" "$fontGreen$text$fontReset"
else
printDotText "A record" "$fontYellow$text$fontReset"
fi
fi
# SPF record
if ! run output error dig +short TXT "$postfixHost" "$dnsResolver"; then
printDotText "SPF record" "$fontRed${output:-$error}$fontReset"
elif grep -Eq '^"?v=spf1([[:space:]]|")' <<<"$output"; then
printDotText "SPF record" "$fontGreen$output$fontReset"
else
printDotText "SPF record" "$fontRed$output$fontReset"
fi
# PTR record
if ! run output error dig -x "$postfixIp" +short "$dnsResolver"; then
printDotText "PTR record" "$fontRed${output:-$error}$fontReset"
else
text=$(printf '%s' "$output" | paste -sd, - | sed 's/,/ \/ /g')
if grep -Fxq -- "$postfixHost." <<<"$output"; then
printDotText "PTR record" "$fontGreen$text$fontReset"
else
printDotText "PTR record" "$fontYellow$text$fontReset"
fi
fi
# DKIM record
label="$postfixLabel DKIM record: $postfixHost"
if ! run output error dig +short TXT "$postfixDkimSelector._domainkey.$postfixHost"; then
printDotText "DKIM record" "$fontRed${output:-$error}$fontReset"
else
local dkimDnsNorm dkimFileRaw dkimFileNorm
dkimDnsNorm=$(
printf '%s\n' "$output" |
tr -d '\n' |
sed -e 's/"//g' -e 's/[[:space:]]//g' |
sed -n 's/.*p=\([^;]*\).*/\1/p'
)
dkimFileRaw=$(postfixDkimGet "$postfixHost")
dkimFileNorm=$(
printf '%s\n' "$dkimFileRaw" |
tr -d '\n' |
sed -e 's/[()"]//g' -e 's/[[:space:]]//g' |
sed -n 's/.*p=\([^;]*\).*/\1/p'
)
if [[ "$dkimDnsNorm" == "$dkimFileNorm" ]]; then
printText "$fontGreen$dkimDnsNorm$fontReset"
else
printText "DNS : $fontYellow$dkimDnsNorm$fontReset"
printText "File: $fontYellow$dkimFileNorm$fontReset"
fi
fi
}
+425
View File
@@ -0,0 +1,425 @@
redisLabel="[Redis]"
redisSentinelLabel="[RedisSentinel]"
redisHaproxyLabel="[RedisHAProxy]"
# Prepares Kubernetes secrets for Redis.
function cmdRedisPreparation() {
printSection "Preparation..."
local error
runError error k3sRun delete secret "$redisKube-secret" --ignore-not-found || {
printDotText "preparation" "$labelFail"
printDanger "Delete old Secret: $error"
return 1
}
runError error k3sRun create secret generic "$redisKube-secret" --from-literal=root-password="$redisRootPass" || {
printDotText "preparation" "$labelFail"
printDanger "Create new Secret: $error"
return 1
}
fileBackup "$redisPath/$redisFileUsersAcl"
printDotText "preparation" "$labelDone"
}
# Renders the Redis Kubernetes YAML manifest via Helm.
function cmdRedisYamlRender() {
local templateFile="templates/$redisKube.yaml"
printSection "Render YAML file | Helm"
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
printDanger "Template file not found"
return 1
}
local profileCpuFile="$appAssetsPath/k3s/profiles/cpu-$kubeCpuProfile.yaml"
printDotText "CPU profile" "$profileCpuFile"
[[ -f "$profileCpuFile" ]] || {
printDanger "CPU profile file not found"
return 1
}
local profileMemoryFile="$appAssetsPath/k3s/profiles/memory-$kubeMemoryProfile.yaml"
printDotText "Memory profile" "$profileMemoryFile"
[[ -f "$profileMemoryFile" ]] || {
printDanger "Memory profile file not found"
return 1
}
local varsFile
varsFile=$(mktemp "/tmp/$redisKube.vars.XXXXXX.yaml") || {
printDotText "render" "$labelFail"
printDanger "Create temp variables file"
return 1
}
printDotText "Variables" "$varsFile"
trap 'rm -f -- "$varsFile"' RETURN
cat > "$varsFile" <<EOF
redisHelm: true
namespace: $k3sNamespace
redis: $redisKube
redisSentinel: $redisSentinelKube
redisPath: $redisPath
redisFileUsersAcl: $redisFileUsersAcl
EOF
printDotText "Result" "$redisYaml"
mkdir -p -- "$(dirname -- "$redisYaml")" || return 1
fileBackup "$redisYaml"
helm template stack "$appAssetsPath/k3s" -f "$varsFile" -f "$profileCpuFile" -f "$profileMemoryFile" --show-only "$templateFile" > "$redisYaml" || {
printDotText "render" "$labelFail"
printDanger "Render failed"
return 1
}
printDotText "render" "$labelDone"
}
# Updates Redis Kubernetes resources (limits, config, etc.) without re-initialization.
function cmdRedisUpdate() {
cmdRedisYamlRender || return 1
cmdK3sYamlApplyEx "$redisYaml" || return 1
}
# Installs Redis into Kubernetes.
function cmdRedisInstall() {
cmdRedisPreparation || return 1
cmdRedisYamlRender || return 1
cmdK3sYamlApplyEx "$redisYaml" || return 1
}
# Uninstalls Redis Kubernetes resources.
function cmdRedisUninstall() {
"$k3sCmd" kubectl delete -f "$redisYaml"
}
# local output error podList ver pid
# if ! run podList error k3sPodListStatus "$olsKube"; then
# printDanger "Get pods: $error"
# elif [[ -z "$podList" ]]; then
# printDanger "Pods not found"
# else
# while IFS='|' read -r pod phase; do
# printSection "$pod"
# if [[ "$phase" != "Running" ]]; then
# printDotText "Phase" "$fontRed$phase$fontReset"
# else
# printDotText "Phase" "$fontGreen$phase$fontReset"
# if ! run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lswsctrl status; then
# printDotText "Status" "$labelUnknown"
# printDanger "$error"
# elif [[ "$output" =~ "running" ]]; then
# printDotText "OpenLiteSpeed status" "$fontGreen$output$fontReset"
# else
# printDotText "OpenLiteSpeed status" "$fontRed$output$fontReset"
# fi
# if run output error openlitespeedPodExec "$pod" /usr/local/lsws/bin/lshttpd -v; then
# ver=$(awk 'NR==1{print $1, $2}' <<< "$output")
# printDotText "OpenLiteSpeed version" "$ver"
# else
# printDotText "OpenLiteSpeed version" "$labelUnknown"
# printDanger "$error"
# fi
# if run output error openlitespeedPodExec "$pod" php -r 'echo PHP_VERSION, "\n";'; then
# printDotText "PHP version" "$output"
# else
# printDotText "PHP version" "$labelUnknown"
# printDanger "$error"
# fi
# fi
# done < <(awk 'NF' <<< "$podList")
# fi
# printRow
# Prints replication INFO for all Redis and Sentinel pods.
function cmdRedisInfo() {
local output error podList ver pid
if ! run podList error k3sPodListStatus "$redisKube"; then
printDanger "Get pods: $error"
elif [[ -z "$podList" ]]; then
printDanger "Pods not found"
else
while IFS='|' read -r pod phase; do
printSection "$pod"
if [[ "$phase" != "Running" ]]; then
printDotText "Phase" "$fontRed$phase$fontReset"
else
printDotText "Phase" "$fontGreen$phase$fontReset"
if ! run output error redisPodExecCli "$pod" INFO server; then
printDotText "Redis version" "$labelUnknown"
printDanger "$error"
else
ver=$(printf '%s' "$output" | grep redis_version | cut -d ':' -f2 | tr -d '[:space:]')
printDotText "Redis version" "$ver"
fi
if ! run output error redisPodExecCli "$pod" INFO replication; then
printDotText "Redis version" "$labelUnknown"
printDanger "$error"
else
local role slaves masterHost masterLinkStatus slaveLag
role=$(printf '%s' "$output" | grep -i "role" | cut -d: -f2 | tr -d '\r\n')
if [[ "$role" == "master" ]]; then
printDotText "Replica role" "$fontGreen$role$fontReset"
slaves=$(printf '%s' "$output" | grep -i "connected_slaves" | cut -d: -f2 | tr -d '\r\n')
if [[ "$slaves" -eq 0 ]]; then
printDotText "Slaves" "$fontRed$slaves$fontReset"
else
printDotText "Slaves" "$fontGreen$slaves$fontReset"
fi
elif [[ "$role" == "slave" ]]; then
printDotText "Replica role" "$fontGreen$role$fontReset"
masterHost=$(printf '%s' "$output" | grep -i "master_host" | cut -d: -f2 | tr -d '\r\n')
masterLinkStatus=$(printf '%s' "$output" | grep -i "master_link_status" | cut -d: -f2 | tr -d '\r\n')
if [[ -z "$masterHost" || "$masterLinkStatus" != "up" ]]; then
[[ -z "$masterHost" ]] && printDotText "Master" "${fontRed}Not connect to master (master_host)$fontReset"
[[ "$masterLinkStatus" != "up" ]] && printDotText "Master" "${fontRed}Not connect to master (master_link_status)$fontReset"
continue
fi
printDotText "Master" "$fontGreen$masterHost$fontReset"
slaveLag=$(printf '%s' "$output" | grep -i "master_last_io_seconds_ago" | cut -d: -f2 | tr -d '\r\n')
[[ "$slaveLag" -ge 3 ]] && printDotText "Replication delay" "$fontYallow${slaveLag}s$fontReset"
else
printDotText "Replica role" "$fontRed$role$fontReset"
fi
fi
fi
done < <(awk 'NF' <<< "$podList")
fi
if ! run podList error k3sPodListStatus "$redisSentinelKube"; then
printDanger "Get pods: $error"
elif [[ -z "$podList" ]]; then
printDanger "Pods not found"
else
local masterInfo slaveInfo
local masterName masterIP masterPort masterNumOtherSentinels masterQuorum
local masterSig replicaSig refPod refMasterSig refReplicaSig
local activeReplicaCount mismatch=0
while IFS='|' read -r pod phase; do
printSection "$pod"
if [[ "$phase" != "Running" ]]; then
printDotText "Phase" "$fontRed$phase$fontReset"
else
printDotText "Phase" "$fontGreen$phase$fontReset"
if ! run output error redisPodExecCli "$pod" INFO server; then
printDotText "RedisSentinel version" "$labelUnknown"
printDanger "$error"
else
ver=$(printf '%s' "$output" | grep redis_version | cut -d ':' -f2 | tr -d '[:space:]')
printDotText "RedisSentinel version" "$ver"
fi
run masterInfo error redisPodExecCli "$pod" SENTINEL masters || {
printDotText "Get sentinel masters" "$labelFail"
printDanger "$error"
mismatch=1
continue
}
local -A masterData=()
while read -r key && read -r value; do
masterData["$key"]="$value"
done <<< "$masterInfo"
masterName="${masterData[name]}"
masterIP="${masterData[ip]}"
masterPort="${masterData[port]}"
masterNumOtherSentinels="${masterData[num-other-sentinels]:-0}"
masterQuorum="${masterData[quorum]:-0}"
run slaveInfo error redisPodExecCli "$pod" --raw sentinel replicas "$masterName" || {
printDotText "Get sentinel replicas" "$labelFail"
printDanger "$error"
mismatch=1
continue
}
replicaSig="$(redisSentinelParseReplicas "$slaveInfo" | awk 'NF' | sort)"
activeReplicaCount="$(awk 'NF {c++} END {print c+0}' <<< "$replicaSig")"
masterSig="${masterName}|${masterIP}|${masterPort}|${activeReplicaCount}|${masterNumOtherSentinels}|${masterQuorum}"
refPod="" refMasterSig="" refReplicaSig=""
if [[ -z "$refPod" ]]; then
refPod="$pod"
refMasterSig="$masterSig"
refReplicaSig="$replicaSig"
fi
if [[ "$masterSig" != "$refMasterSig" || "$replicaSig" != "$refReplicaSig" ]]; then
mismatch=1
printDotText "Topology" "mismatch vs $fontRed$refPod$fontReset"
# else
# printDotText "Topology" "matches $fontGreen$refPod$fontReset"
fi
printDotText " ┌ Replica" "$masterName"
if (( masterQuorum < 1 )); then
printDotText " ├ Quorum" "$fontRed$masterQuorum$fontReset"
else
printDotText " ├ Quorum" "$fontGreen$masterQuorum$fontReset"
fi
printDotText " ├ Other sentinels" "$masterNumOtherSentinels"
# printDotText "Master" "$masterIP:$masterPort"
printDotText " ├ Master" "$masterIP"
if (( activeReplicaCount < 1 )); then
printDotText " └ Slave count" "$fontRed$activeReplicaCount$fontReset"
else
printDotText " └ Slave count" "$fontGreen$activeReplicaCount$fontReset"
fi
while IFS=$'\t' read -r slaveName slaveIP slavePort slaveMaster slaveRunId; do
[[ -z "$slaveName" ]] && continue
printText " ┊"
printDotText " ├ Slave" "$slaveIP"
printDotText " ├ Master" "$slaveMaster"
printDotText " └ RunID" "$slaveRunId"
done <<< "$replicaSig"
fi
done < <(awk 'NF' <<< "$podList")
fi
if ! run podList error k3sPodListStatus "$redisKube-haproxy"; then
printDanger "Get pods: $error"
elif [[ -z "$podList" ]]; then
printDanger "Pods not found"
else
while IFS='|' read -r pod phase; do
printSection "$pod"
if [[ "$phase" != "Running" ]]; then
printDotText "Phase" "$fontRed$phase$fontReset"
else
printDotText "Phase" "$fontGreen$phase$fontReset"
if ! run output error k3sRun exec "pod/$pod" -- haproxy -v; then
printDotText "HAProxy version" "$labelUnknown"
printDanger "$error"
else
ver=$(awk 'NR==1{print $3}' <<< "$output")
printDotText "HAProxy version" "$ver"
fi
local role
if ! run output error redisExecCli -h redis-master -p 6379 ROLE; then
printDotText "Replica role" "$labelFail"
printDanger "$error"
else
role=$(printf '%s' "$output" | head -n1 | tr -d '\r\n')
if [[ "$role" != "master" ]]; then
printDotText "Replica role" "$fontRed$role$fontReset"
else
printDotText "Replica role" "$fontGreen$role$fontReset"
fi
fi
fi
done < <(awk 'NF' <<< "$podList")
fi
}
# Checks replication role/health on each Redis pod and Sentinel topology consistency.
function cmdRedisStatus() {
local output error podList
if ! run podList error k3sPodList "$redisKube"; then
printDanger "$redisLabel Get pods: $error"
elif [[ -z "$podList" ]]; then
printDanger "$redisLabel Pods not found"
else
while read -r pod; do
printSection "$pod"
if run output error redisPodExecCli "$pod" INFO replication; then
printText "$output"
else
printDanger "$redisLabel $pod | $error"
fi
done < <(awk 'NF' <<< "$podList")
fi
if ! run podList error k3sPodList "$redisSentinelKube"; then
printDanger "$redisSentinelLabel Get pods: $error"
elif [[ -z "$podList" ]]; then
printDanger "$redisSentinelLabel Pods not found"
else
while read -r pod; do
printSection "$pod"
if run output error redisPodExecCli "$pod" INFO sentinel; then
printText "$output"
else
printDanger "$redisSentinelLabel $pod | $error"
fi
done < <(awk 'NF' <<< "$podList")
fi
}
# Lists all Redis ACL users.
function cmdRedisUser() {
local output error
printRow
if ! run output error redisUserListGet; then
printDanger "$error"
elif [[ -z "$output" ]]; then
printText "$labelNull"
else
printText "$output"
fi
}
# Flushes all keys from the Redis database.
function cmdRedisDatabaseFlush() {
local output error
printRow
if run output error redisDatabaseFlush; then
printText "$output"
else
printDanger "$error"
fi
}
# Regenerates and applies the Redis root password across all nodes.
function cmdRedisRootPassUpdate() {
local output error
printRow
if run output error redisRootPassUpdate; then
printText "$output"
else
printDanger "$error"
fi
}
+473
View File
@@ -0,0 +1,473 @@
restoreLabel="[Restore]"
# Restores site files from an archive.
#
# The archive is first extracted into a temporary directory located on the same
# filesystem as the target vhost directory. After successful extraction, the
# current vhost directory is moved to a temporary backup path and the restored
# directory is moved into place.
#
# $1 (domain): Site domain name.
# $2 (file): Source archive file path.
#
# Returns:
# 0 on success, 1 on validation or restore failure.
function restoreSiteFiles() {
local domain
domain=$(domainPrepare "$1")
domainCheck "$domain" || return 1
local target="$olsVhostsPath/$domain"
local source="$2"
if [[ -z "$source" ]]; then
appError "Source files not specified"
return 1
fi
if [[ ! -e "$source" ]]; then
appError "Source files not found: $source"
return 1
fi
local tmpDir restoreSource output rc
if [[ -f "$source" ]]; then
tmpDir=$(mktemp -d) || {
appError "Failed to create temporary restore directory"
return 1
}
archiveExtract "$source" "$tmpDir" || {
rm -rf -- "$tmpDir" &>/dev/null
return 1
}
restoreSource="$tmpDir"
else
restoreSource="$source"
fi
rm -rf -- "$target" || {
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "Failed to remove target directory: $target"
return 1
}
mkdir -p -- "$target" || {
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "Failed to create target directory: $target"
return 1
}
output=$(cp -a -- "$restoreSource"/. "$target/" 2>&1)
rc=$?
if [[ $rc -ne 0 ]]; then
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "Failed to copy restored files: $output"
return 1
fi
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
openlitespeedVhostRebuild "$domain" || return 1
return 0
}
# Restores a site database from a SQL file or archive.
#
# If the source file is an archive, it is extracted into a temporary directory
# first and must contain exactly one SQL file. If the target database already
# exists, the SQL file is first imported into a temporary database to validate
# the restore before recreating the target database.
#
# $1 (domain): Site domain name.
# $2 (file): Source SQL file or archive file path.
#
# Returns:
# 0 on success, 1 on validation or restore failure.
function restoreSiteDatabase() {
local domain
domain=$(domainPrepare "$1")
domainCheck "$domain" || return 1
local file="$2"
if [[ -z "$file" ]]; then
appError "Source database file not specified"
return 1
fi
if [[ ! -f "$file" ]]; then
appError "Source database file not found: $file"
return 1
fi
local tmpDir importFile
importFile="$file"
case "$file" in
*.zip|*.tar.gz|*.tgz)
tmpDir=$(mktemp -d) || {
appError "Failed to create temporary database restore directory"
return 1
}
archiveExtract "$file" "$tmpDir" || {
rm -rf -- "$tmpDir" &>/dev/null
return 1
}
local sqlCount
sqlCount=$(find -- "$tmpDir" -type f -name "*.sql" | wc -l)
if [[ "$sqlCount" -ne 1 ]]; then
rm -rf -- "$tmpDir" &>/dev/null
appError "Archive must contain exactly one SQL file: $file"
return 1
fi
importFile=$(find -- "$tmpDir" -type f -name "*.sql" -print -quit)
;;
esac
local databaseName databaseUser databasePass
databaseName=$(siteConfigGetOrSet "$domain" "databaseName" "$(mariadbDomain2id "$domain")")
databaseUser=$(siteConfigGetOrSet "$domain" "databaseUser" "$(mariadbDomain2id "$domain")")
databasePass=$(siteConfigGetOrCreate "$domain" "databasePass")
local dbExists=0
if mariadbDatabaseExists "$databaseName"; then
dbExists=1
fi
local output rc
if (( dbExists == 0 )); then
if ! mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass"; then
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "Failed to create database/user"
return 1
fi
output=$(mariadbMasterImport "$databaseName" "$databaseUser" "$databasePass" "$importFile" 2>&1)
rc=$?
if [[ $rc -ne 0 ]]; then
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "mariadbMasterImport: $output"
return 1
fi
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
return 0
fi
local ts tmpDb
ts=$(date +%Y%m%d_%H%M%S)
tmpDb="_test_${databaseName}_$ts"
if ! mariadbDatabaseUserSet "$tmpDb" "$databaseUser" "$databasePass"; then
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "Failed to prepare tmp database"
return 1
fi
output=$(mariadbMasterImport "$tmpDb" "$databaseUser" "$databasePass" "$importFile" 2>&1)
rc=$?
if [[ $rc -ne 0 ]]; then
mariadbDatabaseRemove "$tmpDb"
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "Failed to import tmp database: $output"
return 1
fi
if ! mariadbDatabaseRemove "$databaseName"; then
mariadbDatabaseRemove "$tmpDb"
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "Failed to recreate target database: remove failed"
return 1
fi
if ! mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass"; then
mariadbDatabaseRemove "$tmpDb"
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "Failed to recreate target database: create failed"
return 1
fi
output=$(mariadbMasterImport "$databaseName" "$databaseUser" "$databasePass" "$importFile" 2>&1)
rc=$?
if [[ $rc -ne 0 ]]; then
mariadbDatabaseRemove "$tmpDb"
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
appError "Failed to import database: $output"
return 1
fi
mariadbDatabaseRemove "$tmpDb"
[[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null
return 0
}
# Restores selected parts of a site from explicit source paths.
#
# Each source is optional. If a source path is provided, the corresponding part
# of the site is restored. If a source path is empty, that part is skipped.
#
# $1 (domain): Site domain name.
# $2 (sourceFiles): Optional source directory or archive file path with site files.
# $3 (sourceDatabase): Optional source SQL file or archive file path with database dump.
# $4 (sourceConf): Optional source OpenLiteSpeed virtual host config file path.
#
# Returns:
# 0 on success, 1 on validation or restore failure.
function restoreSite() {
local domain error
domain=$(domainPrepare "$1")
if ! runError error domainCheck "$domain"; then
printDanger "$siteLabel $error"
return 1
fi
local sourceFiles="$2"
local sourceDatabase="$3"
local sourceConf="$4"
# Files
printInfo "$restoreLabel $domain | Files: $sourceFiles"
if [[ -n "$sourceFiles" ]]; then
if ! runError error restoreSiteFiles "$domain" "$sourceFiles"; then
printDanger "$restoreLabel $domain | Files: $error"
return 1
fi
printSuccess "$restoreLabel $domain | Files: Restoration complete"
else
printWarning "$restoreLabel $domain | Files: Skip restoring"
fi
# Database
printInfo "$restoreLabel $domain | Database: $sourceDatabase"
if [[ -n "$sourceDatabase" ]]; then
if ! runError error restoreSiteDatabase "$domain" "$sourceDatabase"; then
printDanger "$restoreLabel $domain | Database: $error"
return 1
fi
printSuccess "$restoreLabel $domain | Database: Restoration complete"
else
printWarning "$restoreLabel $domain | Database: Skip restoring"
fi
# Config
printInfo "$restoreLabel $domain | Config: $sourceConf"
if [[ -n "$sourceConf" ]]; then
if ! runError error cp -f -- "$sourceConf" "$olsVhostsConfigPath/$domain.conf"; then
printDanger "$restoreLabel $domain | Config: $error"
return 1
fi
printSuccess "$restoreLabel $domain | Config: Restoration complete"
else
printWarning "$restoreLabel $domain | Config: Skip restoring"
fi
return 0
}
# Restores a site from available backup entries.
#
# The function searches short-term and long-term backup paths for files,
# database dumps, and OpenLiteSpeed virtual host configuration files matching
# the specified domain. It can list available backups or restore a selected one.
#
# Supported index values:
# empty: Show available backups and ask for a backup number.
# list: Print available backup paths.
# last: Restore the latest available backup.
# full: Restore the latest backup that contains files, database, and config.
# auto: Restore the latest full backup if available, otherwise the latest backup.
# N: Restore backup by numeric index.
#
# $1 (domain): Site domain name.
# $2 (index): Optional backup selector: list, last, full, auto, or numeric index.
#
# Returns:
# 0 on success, 1 on validation, selection, or restore failure.
function restoreRun() {
local domain error
domain=$(domainPrepare "$1")
if ! runError error domainCheck "$domain"; then
printDanger "$siteLabel $error"
return 1
fi
local index="$2"
if [[ -n "$index" && ! "$index" =~ ^[0-9]+$ && "$index" != "auto" && "$index" != "last" && "$index" != "full" && "$index" != "list" ]]; then
printDanger "Unknown value of index"
return 1
fi
local backupEntryList=()
local backupPathList=()
local path
shopt -s nullglob
local entryList=("$backupDailyPath"/*/*/"$domain"*)
shopt -u nullglob
for entry in "${entryList[@]}"; do
backupEntryList+=("$entry")
path=2:$(dirname -- "$entry")
if ! arrayContains "$path" "${backupPathList[@]}"; then
backupPathList+=("$path")
fi
done
shopt -s nullglob
local entryList=("$backupArchivePath"/*/*/"$domain"*)
shopt -u nullglob
for entry in "${entryList[@]}"; do
backupEntryList+=("$entry")
path=1:$(dirname -- "$entry")
if ! arrayContains "$path" "${backupPathList[@]}"; then
backupPathList+=("$path")
fi
done
local backupSortList=($(printf "%s\n" "${backupPathList[@]}" | \
sed "s/\/${backupFirstDir}$/\/./" | \
sort -r | \
sed "s/\/.$/\/${backupFirstDir}/"))
local bType bPath bTime bDate sourceList indexFull suffix
local counter=1
for marker in "${backupSortList[@]}"; do
bType="${marker:0:1}"
bPath="${marker:2}"
bTime=$(basename "$bPath")
bDate=$(basename -- $(dirname -- "$bPath"))
sourceList=()
if arrayContains "$bPath/$domain" "${backupEntryList[@]}"; then
sourceList+=('files:dir')
fi
if arrayContains "$bPath/$domain.tar.gz" "${backupEntryList[@]}"; then
sourceList+=('files:tar')
fi
if arrayContains "$bPath/$domain.tgz" "${backupEntryList[@]}"; then
sourceList+=('files:tar')
fi
if arrayContains "$bPath/$domain.zip" "${backupEntryList[@]}"; then
sourceList+=('files:zip')
fi
if arrayContains "$bPath/$domain.sql" "${backupEntryList[@]}"; then
sourceList+=('db:sql')
fi
if arrayContains "$bPath/$domain.sql.tar.gz" "${backupEntryList[@]}"; then
sourceList+=('db:tar')
fi
if arrayContains "$bPath/$domain.sql.tgz" "${backupEntryList[@]}"; then
sourceList+=('db:tar')
fi
if arrayContains "$bPath/$domain.sql.zip" "${backupEntryList[@]}"; then
sourceList+=('db:zip')
fi
if arrayContains "$bPath/$domain.conf" "${backupEntryList[@]}"; then
sourceList+=('conf')
fi
if arrayContains "conf" "${sourceList[@]}" && \
( arrayContains "db:sql" "${sourceList[@]}" || arrayContains "db:tar" "${sourceList[@]}" || arrayContains "db:zip" "${sourceList[@]}" ) && \
( arrayContains "files:dir" "${sourceList[@]}" || arrayContains "files:tar" "${sourceList[@]}" || arrayContains "files:zip" "${sourceList[@]}" ); then
suffix="\033[34m${sourceList[*]}\033[0m"
if [[ -z "$indexFull" ]]; then
indexFull="$counter"
fi
else
suffix="${sourceList[*]}"
fi
if [[ "$bType" == "1" ]]; then
suffix+=" | \033[33mLongTerm\033[0m"
fi
if [[ -z "$index" ]]; then
printf "%2s: %-19s | $suffix\n" "$counter" "$bDate/$bTime"
fi
((counter++))
done
local indexSelect=
case "$index" in
list)
for marker in "${backupSortList[@]}"; do
printf "%s\n" "${marker:2}"
done
return 0
;;
last)
indexSelect=1
;;
full)
indexSelect="$indexFull"
;;
auto)
if [[ -n "$indexFull" ]]; then
indexSelect="$indexFull"
else
indexSelect=1
fi
;;
[0-9]*)
indexSelect="$index"
;;
*)
printWarning "$domain" "Warning! The relevant data will be cleared before restoring."
read -p "Specify the backup number: " indexSelect
;;
esac
((indexSelect--)) 2>/dev/null
if [[ "$indexSelect" -lt 0 || "$indexSelect" -ge "${#backupSortList[@]}" ]]; then
printDanger "Unknown index number"
return 1
fi
local restorePath="${backupSortList[$indexSelect]:2}"
if [[ ! -d "$restorePath" ]]; then
printDanger "Path for restore not found"
return 1
fi
local sourceFiles sourceDatabase sourceConf
if [[ -d "$restorePath/$domain" ]]; then
sourceFiles="$restorePath/$domain"
elif [[ -f "$restorePath/$domain.tar.gz" ]]; then
sourceFiles="$restorePath/$domain.tar.gz"
elif [[ -f "$restorePath/$domain.tgz" ]]; then
sourceFiles="$restorePath/$domain.tgz"
elif [[ -f "$restorePath/$domain.zip" ]]; then
sourceFiles="$restorePath/$domain.zip"
fi
if [[ -f "$restorePath/$domain.sql" ]]; then
sourceDatabase="$restorePath/$domain.sql"
elif [[ -f "$restorePath/$domain.sql.tar.gz" ]]; then
sourceDatabase="$restorePath/$domain.sql.tar.gz"
elif [[ -f "$restorePath/$domain.sql.tgz" ]]; then
sourceDatabase="$restorePath/$domain.sql.tgz"
elif [[ -f "$restorePath/$domain.sql.zip" ]]; then
sourceDatabase="$restorePath/$domain.sql.zip"
fi
if [[ -f "$restorePath/$domain.conf" ]]; then
sourceConf="$restorePath/$domain.conf"
fi
restoreSite "$domain" "$sourceFiles" "$sourceDatabase" "$sourceConf"
# Clean Redis
redisDomainClean "$domain"
# Rebuild config site
cmdSiteConfigRebuild "$domain"
# Rebuild config Wordpress
wordpressConfigRebuild "$domain"
return 0
}
+733
View File
@@ -0,0 +1,733 @@
# Prints a confirmation and returns 0 if confirmed.
# $1 (query): query
function cmdRouterConfirm() {
local query="$1" confirmation
printRow
read -r -p "${fontYellow}Warning!${fontReset} $query (y/n): " confirmation
[[ "$confirmation" =~ ^[Yy]$ ]]
}
function cmdK3s() {
local action="${1:-}"
shift || true
case "$action" in
install)
printTitle " $k3sLabel Install"
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue ALL$fontReset resources to k3s?"; then
cmdMariadbInstall
cmdRedisInstall
cmdOpenlitespeedInstall
cmdPostfixInstall
cmdWorkerInstall
cmdMetricInstall
fi
;;
uninstall)
printTitle " $k3sLabel Uninstall"
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue ALL$fontReset resources from k3s?"; then
cmdK3sResourceClean
fi
;;
info)
printTitle " $k3sLabel Info"
cmdK3sInfo
;;
status)
printTitle " $k3sLabel Status"
cmdK3sNodesStatus
;;
top)
printTitle " $k3sLabel Top pod"
cmdK3sTopPod
;;
res)
printTitle " $k3sLabel Resources $@"
cmdK3sResourceList "$@"
;;
*)
printTitle " $k3sLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpK3S
;;
esac
}
function cmdMariadb() {
local action="${1:-}"
shift || true
case "$action" in
install)
printTitle " $mariadbLabel Install"
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue MariaDB$fontReset resources to k3s?"; then
cmdMariadbInstall
fi
;;
update)
printTitle " $mariadbLabel Update"
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue MariaDB$fontReset resources in k3s?"; then
cmdMariadbUpdate
fi
;;
uninstall)
printTitle " $mariadbLabel Uninstall"
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue MariaDB$fontReset resources from k3s?"; then
cmdMariadbUninstall
fi
;;
info)
printTitle " $mariadbLabel Info"
cmdMariadbInfo
;;
status)
printTitle " $mariadbLabel Status"
cmdMariadbStatus
;;
replica)
printTitle " $mariadbLabel Replica rebuild"
if cmdRouterConfirm "Are you sure you want to$fontRed REBUILD$fontBlue MariaDB replica$fontReset?"; then
cmdMariadbReplicaRebuild
fi
;;
database)
printTitle " $mariadbLabel Database list"
cmdMariadbDatabase
;;
user)
printTitle " $mariadbLabel User list"
cmdMariadbUser
;;
dump)
printTitle " $mariadbLabel Dump Master $@"
cmdMariadbMasterDump "$@"
;;
dump_slave)
printTitle " $mariadbLabel Dump Slave $@"
cmdMariadbSlaveDump "$@"
;;
*)
printTitle " $mariadbLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpMariaDB
;;
esac
}
function cmdRedis() {
local action="${1:-}"
shift || true
case "$action" in
install)
printTitle " $redisLabel Install"
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue Redis$fontReset resources to k3s?"; then
cmdRedisInstall
fi
;;
update)
printTitle " $redisLabel Update"
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue Redis$fontReset resources in k3s?"; then
cmdRedisUpdate
fi
;;
uninstall)
printTitle " $redisLabel Uninstall"
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue Redis$fontReset resources from k3s?"; then
cmdRedisUninstall
fi
;;
info)
printTitle " $redisLabel Info"
cmdRedisInfo
;;
status)
printTitle " $redisLabel Status"
cmdRedisStatus
;;
user)
printTitle " $redisLabel User list"
cmdRedisUser
;;
flush)
printTitle " $redisLabel Flush current DB"
cmdRedisDatabaseFlush
;;
pass)
printTitle " $redisLabel Update default (root) pass"
cmdRedisRootPassUpdate
;;
*)
printTitle " $redisLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpRedis
;;
esac
}
function cmdOpenlitespeed() {
local action="${1:-}"
shift || true
case "$action" in
install)
printTitle " $openlitespeedLabel Install"
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue OpenLiteSpeed$fontReset resources to k3s?"; then
cmdOpenlitespeedInstall
fi
;;
update)
printTitle " $openlitespeedLabel Update"
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue OpenLiteSpeed$fontReset resources in k3s?"; then
cmdOpenlitespeedUpdate
fi
;;
uninstall)
printTitle " $openlitespeedLabel Uninstall"
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue OpenLiteSpeed$fontReset resources from k3s?"; then
cmdOpenlitespeedUninstall
fi
;;
info)
printTitle " $openlitespeedLabel Info"
cmdOpenlitespeedInfo
;;
list)
printTitle " $openlitespeedLabel List (all|up|down)"
cmdOpenlitespeedSiteList "$@"
;;
up)
printTitle " $openlitespeedLabel Up $@"
cmdOpenlitespeedVhostUp "$@"
;;
down)
printTitle " $openlitespeedLabel Down $@"
cmdOpenlitespeedVhostDown "$@"
;;
alias)
printTitle " $openlitespeedLabel Alias $1"
cmdOpenlitespeedVhostAliasSet "$@"
;;
restart)
printTitle " $openlitespeedLabel Restart"
cmdOpenlitespeedRestart
;;
php_kill)
printTitle " $openlitespeedLabel Kill PHP $@"
cmdOpenlitespeedPhpKill "$@"
;;
white_list)
printTitle " $openlitespeedLabel White list update"
cmdOpenlitespeedAdminWhiteList
;;
allow_list)
printTitle " $openlitespeedLabel Allow list update"
cmdOpenlitespeedAdminAllowList
;;
alias_list)
printTitle " $openlitespeedLabel Alias list $1"
cmdOpenlitespeedAliasList "$@"
;;
rebuild)
printTitle " $openlitespeedLabel Rebuild $1"
cmdOpenlitespeedVhostRebuild "$@"
;;
config)
printTitle " $openlitespeedLabel Config check"
cmdOpenlitespeedConfigCheck
;;
*)
printTitle " $openlitespeedLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpOpenLiteSpeed
;;
esac
}
function cmdPostfix() {
local action="${1:-}"
shift || true
case "$action" in
install)
printTitle " $postfixLabel Install"
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue Postfix$fontReset resources to k3s?"; then
cmdPostfixInstall
fi
;;
update)
printTitle " $postfixLabel Update"
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue Postfix$fontReset resources in k3s?"; then
cmdPostfixUpdate
fi
;;
uninstall)
printTitle " $postfixLabel Uninstall"
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue Postfix$fontReset resources from k3s?"; then
cmdPostfixUninstall
fi
;;
info)
printTitle " $postfixLabel Info"
cmdPostfixInfo
;;
status)
printTitle " $postfixLabel Status"
cmdPostfixMtaDnsCheck
;;
user)
printTitle " $postfixLabel User list"
cmdPostfixUser
;;
dkim)
printInfo " $postfixLabel DKIM record for DNS (autocreate)"
postfixDkimGet "$@"
;;
*)
printTitle " $postfixLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpPostfix
;;
esac
}
function cmdWorker() {
local action="${1:-}"
shift || true
case "$action" in
install)
printTitle " $workerLabel Install"
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue Worker$fontReset resources to k3s?"; then
cmdWorkerInstall
fi
;;
update)
printTitle " $workerLabel Update"
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue Worker$fontReset resources in k3s?"; then
cmdWorkerUpdate
fi
;;
uninstall)
printTitle " $workerLabel Uninstall"
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue Worker$fontReset resources from k3s?"; then
cmdWorkerUninstall
fi
;;
task)
printInfo " $workerLabel Run $1"
reportFile="$logPath/task/${appDate}_$appTime.log"
printText "Start: $appDate $appTime\n"
cmdWorkerTaskHandle "$@"
printText "\nFinish: $(date +%Y-%m-%d) $(date +%H-%M-%S) | Time: $(( $(date +%s) - scriptStart ))s"
reportFile=""
;;
list)
printTitle " $workerLabel Task list"
cmdWorkerTaskList
;;
down)
printInfo " $workerLabel Put on pause..."
workerAgentStop
;;
up)
printInfo " $workerLabel Resuming from pause..."
workerAgentStart
;;
*)
printTitle " $workerLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpWorker
;;
esac
}
function cmdMetric() {
local action="${1:-}"
shift || true
case "$action" in
install)
printTitle " $metricLabel Install"
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontBlue Metric$fontReset resources to k3s?"; then
cmdMetricInstall
fi
;;
update)
printTitle " $metricLabel Update"
if cmdRouterConfirm "Are you sure you want to$fontRed UPDATE$fontBlue Metric$fontReset resources in k3s?"; then
cmdMetricUpdate
fi
;;
uninstall)
printTitle " $metricLabel Uninstall"
if cmdRouterConfirm "Are you sure you want to$fontRed UNINSTALL$fontBlue Metric$fontReset resources from k3s?"; then
cmdMetricUninstall
fi
;;
restart)
printInfo " $metricLabel Restart"
metricRestart
;;
*)
printTitle " $metricLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpMetric
;;
esac
}
function cmdSite() {
local action="${1:-}"
shift || true
case "$action" in
add)
printTitle " $siteLabel Add $*"
cmdSiteAdd "$@"
;;
add_wp|wp)
printTitle " $siteLabel Add Wordpress $*"
cmdSiteAddWordpress "$@"
;;
remove)
printTitle " $siteLabel Remove $*"
cmdSiteRemove "$@"
;;
copy)
printTitle " $siteLabel Copy $*"
cmdSiteCopy "$@"
;;
rename)
printTitle " $siteLabel Rename $*"
cmdSiteRename "$@"
;;
rebuild)
printTitle " $siteLabel Rebuild config $*"
cmdSiteConfigRebuild "$@"
;;
rebuild_wp)
printTitle " $siteLabel Wordpress config rebuild $*"
cmdSiteWordpressRebuild "$@"
;;
reset_pass)
printTitle " $siteLabel Reset ALL passwords for vhost $*"
cmdSitePasswordReset "$@"
;;
reset_auth)
printTitle " $siteLabel Reset ALL auth settings for vhost $*"
cmdSiteAuthReset "$@"
;;
dump)
printTitle " $siteLabel Database dump $*"
cmdSiteDatabaseDump "$@"
;;
info)
printTitle " $siteLabel Info $*"
cmdSiteInfo "$@"
;;
status)
printTitle " $siteLabel Status $*"
cmdSiteStatus "$@"
;;
*)
printTitle " $siteLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpSite
;;
esac
}
function cmdWordpress() {
local action="${1:-}"
shift || true
case "$action" in
user)
printTitle " $wordpressLabel User list"
cmdWordpressUserList "$@"
;;
pass)
printTitle " $wordpressLabel User password set"
cmdWordpressPasswordSet "$@"
;;
salt)
printTitle " $wordpressLabel Shuffle salts $*"
cmdWordpressSalt "$@"
;;
check)
printTitle " $wordpressLabel Check core and plugins $*"
cmdWordpressCheck "$@"
;;
exec)
printTitle " $wordpressLabel Command exec $*"
cmdWordpressExec "$@"
;;
*)
printTitle " $wordpressLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpWP
;;
esac
}
function cmdSftp() {
local action="${1:-}"
shift || true
case "$action" in
user)
printTitle " $systemLabel SFTP User list $*"
cmdSftpUserList "$@"
;;
enable)
printTitle " $systemLabel SFTP Access enable $*"
cmdSftpAccessEnable "$@"
;;
disable)
printTitle " $systemLabel SFTP Access disable $*"
cmdSftpAccessDisable "$@"
;;
reset_pass)
printTitle " $systemLabel SFTP Reset pass"
cmdSftpPasswordSet "$@"
;;
support)
printTitle " $systemLabel SFTP Adding support"
if cmdRouterConfirm "Changes will be made to the$fontRed SSH configuration$fontReset. Make sure there is a$fontBlue backup way to connect$fontReset to the server. Continue?"; then
cmdSftpAddingSupport
fi
;;
*)
printTitle " $systemLabel SFTP"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpSftp
;;
esac
}
function cmdCron() {
local action="${1:-}"
shift || true
case "$action" in
add)
printTitle " $systemLabel Cron job add"
cmdCronAdd
;;
remove)
printTitle " $systemLabel Cron job remove"
cmdCronRemove
;;
list)
printTitle " $systemLabel Cron job list"
cmdCronList
;;
*)
printTitle " $systemLabel Cron"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpCron
;;
esac
}
function cmdBackup() {
local action="${1:-}"
shift || true
case "$action" in
run)
printTitle " $systemLabel Backup of target"
cmdBackupRun "$@"
;;
list)
printTitle " $systemLabel List of backups on local storage"
cmdBackupList
;;
size)
printTitle " $systemLabel List of Backup Sizes"
cmdBackupSize
;;
# remove)
# printTitle " $backupLabel Remove of backups on local storage"
# cmdStorageBackupRemove
# ;;
*)
printTitle " $systemLabel Backup"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpBackup
;;
esac
}
function cmdRestore() {
local action="${1:-}"
shift || true
case "$action" in
run)
printTitle " $restoreLabel Backup of target"
restoreRun "$@"
;;
*)
printTitle " $restoreLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpRestore
;;
esac
}
function cmdStorage() {
local action="${1:-}"
shift || true
case "$action" in
list)
printTitle " $storageLabel List of backups on external storage"
cmdStorageBackupList
;;
compare)
printTitle " $systemLabel Comparison table"
cmdStorageBackupCompare
;;
size)
printTitle " $systemLabel List of Backup Sizes on external storage"
cmdStorageBackupSize
;;
sync)
printTitle " $systemLabel Synchronization with external storage"
cmdStorageBackupSync "$@"
;;
remove)
printTitle " $systemLabel Remove of backups on external storage"
cmdStorageBackupRemove
;;
*)
printTitle " $systemLabel Storage"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpStorage
;;
esac
}
# Dispatches a named script sub-command and redirects output to a timestamped log file.
# $1 (option): script name (backup, mariadb-dump, worker-observer, metric-kube, metric-sites, diag-report-ai-short, diag-report-ai-full).
function cmdScript() {
cmdScriptRun "$@"
}
function cmdInstall() {
printTitle " $ks3Label Full install"
if cmdRouterConfirm "Are you sure you want to$fontRed INSTALL$fontReset $fontBlue FULL infrastrutute$fontReset?"; then
cmdInstallFull
fi
}
function cmdTest() {
local action="${1:-}"
shift || true
case "$action" in
mariadb)
printInfo " $testLabel MariaDB replica"
testMariadbReplica
;;
redis)
printInfo " $testLabel Redis cluster"
testRedisCluster
;;
site)
printInfo " $testLabel Site"
testSite
;;
wordpress)
printInfo " $testLabel Wordpress"
testSiteWordpress
;;
*)
printTitle " $testLabel SFTP"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpTest
;;
esac
}
function cmdMalware() {
local action="${1:-}"
shift || true
case "$action" in
user)
printTitle " Malware user list"
cmdMalwareUserList
;;
plugin)
printTitle " Malware plugin list"
cmdMalwareMuPluginList
;;
file)
printTitle " Malware files list"
cmdMalwareFilesCheck "$@"
;;
*)
printTitle " $testLabel Malware"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpMalware
;;
esac
}
function cmdRouter() {
local action="${1:-}"
shift || true
printHeader
case "$action" in
-k|--k3s) cmdK3s "$@" ;;
-m|--mariadb) cmdMariadb "$@" ;;
-r|--redis) cmdRedis "$@" ;;
-o|--ols) cmdOpenlitespeed "$@" ;;
-p|--postfix) cmdPostfix "$@" ;;
-w|--worker) cmdWorker "$@" ;;
-s|--site) cmdSite "$@" ;;
--metric) cmdMetric "$@" ;;
--wp) cmdWordpress "$@" ;;
--sftp) cmdSftp "$@" ;;
--cron) cmdCron "$@" ;;
--shell) cmdShell "$@" ;;
--log) cmdLog "$@" ;;
--describe) cmdDescribe "$@" ;;
--delete) cmdDelete "$@" ;;
--backup) cmdBackup "$@" ;;
--restore) cmdRestore "$@" ;;
--storage) cmdStorage "$@" ;;
--script) cmdScript "$@" ;;
--install) cmdInstall "$@" ;;
--test) cmdTest "$@" ;;
--malware) cmdMalware "$@" ;;
--help) cmdHelp "$@" ;;
dev) appDev "$@" ;;
esac
local status=$?
printFooter
return "$status"
}
+104
View File
@@ -0,0 +1,104 @@
scriptLabel="[Script]"
# Runs the site backup dispatch with script logging.
function cmdScriptBackup() {
printDotText "Script" "Backup sites"
printStart
cmdBackupDispatch
printFinish
}
# Runs the MariaDB master full-dump script with script logging.
function cmdScriptMariadbDump() {
printDotText "Script" "MariaDB database dump"
printStart
cmdMariadbMasterDump
printFinish
}
# Runs the worker task observer with script logging.
function cmdScriptWorkerObserver() {
printDotText "Script" "Worker observer"
printStart
cmdWorkerObserver
printFinish
}
# Collects and pushes kube and lsphp metrics with script logging.
function cmdScriptMetricKube() {
printDotText "Script" "Metric KUBE"
printStart
metricKube
metricLsphp
printFinish
}
# Collects and pushes per-site metrics with script logging.
function cmdScriptMetricSites() {
printDotText "Script" "Metric sites"
printStart
metricSites
printFinish
}
# Runs the AI diagnostic report with script logging.
# [$1] (mode): report mode: short (default) or full.
function cmdScriptDiagReportAi() {
local mode
mode=${1:-short}
printDotText "Script" "Diag report AI $mode"
printStart
diagRun "$mode"
printFinish
}
function cmdScriptRun() {
local option="$1"
printText "Output: $logPath/$option/${appDate}_$appTime.log"
printRow
printFile="$logPath/$option/${appDate}_$appTime.log"
local logFileOld="$logFile"
logFile=""
case "$option" in
backup)
cmdScriptBackup
;;
mariadb-dump)
cmdScriptMariadbDump
;;
worker-observer)
cmdScriptWorkerObserver
;;
metric-kube)
cmdScriptMetricKube
;;
metric-sites)
cmdScriptMetricSites
;;
diag-report-ai-short)
cmdScriptDiagReportAi "short"
;;
diag-report-ai-full)
cmdScriptDiagReportAi "full"
;;
unigma)
cmdUnigma
;;
*)
printFile=""
logFile="$logFileOld"
printTitle " $scriptLabel"
printRow
printWarning "Unsupported or empty command: $action"
cmdHelpScript
;;
esac
printFile=""
logFile="$logFileOld"
}
+837
View File
@@ -0,0 +1,837 @@
siteLabel="[Site]"
siteWordpressLabel="[Wordpress]"
# Creates a new site: validates domain/source/DB, creates OLS vhost, copies files,
# sets up MariaDB/Redis/Postfix, rolls back via cmdSiteRemove on failure.
# $1 (domain): site domain name.
# $2 (sourceFiles): source directory or archive with site files.
# [$3] (sourceDatabase): optional SQL dump or archive with SQL dump.
function cmdSiteAdd() {
local domain sourceFiles
domain=$(domainPrepare "$1")
shift || true
sourceFiles="${1:-$appAssetsPath/vhost/default}"
shift || true
printSection "Add site"
printDotText "domain" "$domain"
if ! runError error domainCheck "$domain"; then
printDotText "status" "$labelFail"
printDanger "$error"
elif ! run output error siteAdd "$domain" "$sourceFiles" "$@"; then
printDotText "status" "$labelFail"
printDanger "$error"
else
printDotText "status" "$labelDone"
cmdOpenlitespeedRestart
fi
}
# Removes a site and all associated MariaDB/Redis/Postfix/OLS/host resources.
# $1 (domain): site domain name.
function cmdSiteRemove() {
local domain mode
domain=$(domainPrepare "$1")
shift || true
mode="$1"
shift || true
if [[ ! "$mode" == "-f" && ! "$mode" == "--force" ]]; then
if ! cmdRouterConfirm "Are you sure you want to$fontRed DELETE$fontReset the site $fontBlue$domain$fontReset?"; then
printRow
return 0
fi
fi
printSection "Remove site"
printDotText "domain" "$domain"
if ! runError error domainCheck "$domain"; then
printDotText "status" "$labelFail"
printDanger "$error"
elif ! run output error siteRemove "$domain"; then
printDotText "status" "$labelFail"
printDanger "$error"
else
printDotText "status" "$labelDone"
cmdOpenlitespeedRestart
fi
}
# 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 cmdSiteCopy() {
local domain domainNew
domain=$(domainPrepare "$1")
domainNew=$(domainPrepare "$2")
printSection "Copy site"
printDotText "source" "$domain"
printDotText "target" "$domainNew"
if ! runError error domainCheck "$domain"; then
printDotText "status" "$labelFail"
printDanger "$error"
elif ! runError error domainCheck "$domainNew"; then
printDotText "status" "$labelFail"
printDanger "$error"
elif ! run output error siteCopy "$domain" "$domainNew"; then
printDotText "status" "$labelFail"
printDanger "$error"
else
printDotText "status" "$labelDone"
cmdOpenlitespeedRestart
if ! runError error wordpressDomainUpdate "$domainNew"; then
printDotText "update" "$labelFail"
printDanger "$error"
else
printDotText "update" "$labelDone"
fi
fi
}
# 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 cmdSiteRename() {
local domain domainNew
domain=$(domainPrepare "$1")
domainNew=$(domainPrepare "$2")
printSection "Rename site"
printDotText "source" "$domain"
printDotText "target" "$domainNew"
if ! runError error domainCheck "$domain"; then
printDotText "status" "$labelFail"
printDanger "$error"
elif ! runError error domainCheck "$domainNew"; then
printDotText "status" "$labelFail"
printDanger "$error"
elif ! run output error siteRename "$domain" "$domainNew"; then
printDotText "status" "$labelFail"
printDanger "$error"
else
printDotText "status" "$labelDone"
cmdOpenlitespeedRestart
if ! runError error wordpressDomainUpdate "$domainNew"; then
printDotText "update" "$labelFail"
printDanger "$error"
else
printDotText "update" "$labelDone"
fi
fi
}
# Creates a WordPress site from the bundled template, then rebuilds WP config.
# $1 (domain): site domain name.
# [$2] (sourceFiles): optional source directory or archive.
# [$@] (...): optional remaining arguments passed to cmdSiteAdd (e.g. database dump).
function cmdSiteAddWordpress() {
local domain sourceFiles
domain=$(domainPrepare "$1")
shift || true
sourceFiles="${1:-$appAssetsPath/vhost/wordpress}"
if [[ ! -e "$sourceFiles" ]]; then
sourceFiles="$appAssetsPath/vhost/wordpress.tar.gz"
fi
shift || true
printSection "Add site (Wordpress)"
printDotText "domain" "$domain"
if ! runError error domainCheck "$domain"; then
printDotText "status" "$labelFail"
printDanger "$error"
elif ! run output error siteAdd "$domain" "$sourceFiles" "$@"; then
printDotText "status" "$labelFail"
printDanger "$error"
else
printDotText "status" "$labelDone"
cmdOpenlitespeedRestart
if ! run output error wordpressConfigRebuild "$domain"; then
printDanger "$error"
fi
fi
}
# Rebuilds OLS vhost/config, MariaDB, Redis, Postfix, and WordPress salt for a site.
# $1 (domain): site domain name.
function cmdSiteConfigRebuild() {
local domain
domain=$(domainPrepare "$1")
domainCheck "$domain" || return 1
printRow
if ! runError error openlitespeedVhostRebuild "$domain"; then
printDotText "OLS vhost" "$labelFail"
printDanger "$error"
else
printDotText "OLS vhost" "$labelDone"
fi
if ! runError error openlitespeedConfigVhostSet "$domain"; then
printDotText "OLS config" "$labelFail"
printDanger "$error"
else
printDotText "OLS config" "$labelDone"
fi
if ! runError error mariadbConfigRebuild "$domain"; then
printDotText "MariaDB" "$labelFail"
printDanger "$error"
else
printDotText "MariaDB" "$labelDone"
fi
if ! runError error redisConfigRebuild "$domain"; then
printDotText "Redis" "$labelFail"
printDanger "$error"
else
printDotText "Redis" "$labelDone"
fi
if ! runError error postfixConfigRebuild "$domain"; then
printDotText "Postfix" "$labelFail"
printDanger "$error"
else
printDotText "Postfix" "$labelDone"
fi
if ! runError error wordpressExec "$domain" config shuffle-salts; then
printDotText "Wordpress salt" "$labelFail"
printDanger "$error"
else
printDotText "Wordpress salt" "$labelDone"
fi
}
# Rebuilds the WordPress wp-config.php for a site.
# $1 (domain): site domain name.
function cmdSiteWordpressRebuild() {
local error
printRow
if ! runError error wordpressConfigRebuild "$@"; then
printDotText "Wordpress config rebuild" "$labelFail"
printDanger "$error"
else
printDotText "Wordpress config rebuild" "$labelDone"
fi
}
# Resets databasePass/redisPass/postfixPass and rebuilds configs for one site or all sites.
# Skips sites without wp-config.php in "all" mode.
# $1 (target): site domain name or "all".
function cmdSitePasswordReset() {
local target="$1"
local vhostList error
printRow
if [[ -z "$target" ]]; then
printDanger "Target not specified";
elif ! run vhostList error openlitespeedConfigVhostList; then
printDanger "Cannot get vhost list: $error";
elif [[ "$target" == "all" ]]; then
local domain
for domain in $vhostList; do
if ! runError error sitePasswordReset "$domain"; then
printDotText "$domain" "$labelFail"
printDanger "$error"
else
printDotText "$domain" "$labelDone"
fi
done
elif ! listContains "$target" "$vhostList"; then
printDanger "Unknown domain: $target";
elif ! runError error sitePasswordReset "$target"; then
printDotText "$target" "$labelFail"
printDanger "$error"
else
printDotText "$target" "$labelDone"
fi
}
# Resets all service credentials (passwords + usernames) and rebuilds configs for one site or all sites.
# Skips sites without wp-config.php in "all" mode.
# $1 (target): site domain name or "all".
function cmdSiteAuthReset() {
local target="$1"
local vhostList error
printRow
if [[ -z "$target" ]]; then
printDanger "Target not specified";
elif ! run vhostList error openlitespeedConfigVhostList; then
printDanger "Cannot get vhost list: $error";
elif [[ "$target" == "all" ]]; then
local domain
for domain in $vhostList; do
if ! runError error siteAuthReset "$domain"; then
printDotText "$domain" "$labelFail"
printDanger "$error"
else
printDotText "$domain" "$labelDone"
fi
done
elif ! listContains "$target" "$vhostList"; then
printDanger "Unknown domain: $target";
elif ! runError error siteAuthReset "$target"; then
printDotText "$target" "$labelFail"
printDanger "$error"
else
printDotText "$target" "$labelDone"
fi
}
# Exports a site's database to a file.
# $1 (domain): site domain name.
# [$2] (file): target dump file (auto-generated if omitted).
function cmdSiteDatabaseDump() {
local domain error
domain=$(domainPrepare "$1")
if ! runError error domainCheck "$domain"; then
printDanger "$siteLabel $error"
return 1
fi
local file="$2"
if [[ -z "$file" ]]; then
file="$appDataPath/mariadb/${appDate}_${appTime}_$domain.sql.tar.gz"
fi
local databaseName databaseUser databasePass
databaseName=$(siteConfigGet "$domain" "databaseName")
databaseUser=$(siteConfigGet "$domain" "databaseUser")
databasePass=$(siteConfigGet "$domain" "databasePass")
printRow
printDotText "file" "$file"
printDotText "base" "$databaseName"
printDotText "user" "$databaseUser"
if ! runError error mariadbMasterExport "$databaseUser" "$databasePass" "$databaseName" "$file"; then
printDotText "status" "$labelFailed"
printDanger "$error"
else
printDotText "status" "$labelDone"
fi
}
# Prints system, config, and OLS details for a site.
# $1 (domain): site domain name.
function cmdSiteInfo() {
printRow
local domain error
domain=$(domainPrepare "$1")
if ! runError error domainCheck "$domain"; then
printDanger "$error"
return 1
fi
printSection "System"
local ug userId groupId
ug=$(domainToUser "$domain")
printDotText "user" "$ug"
printDotText "group" "$ug"
if ! run userId error id -u "$ug"; then
printDotText "userID" "$labelUnknown"
else
printDotText "userID" "$fontGreen$userId$fontReset"
fi
if ! run groupId error id -g "$ug"; then
printDotText "groupID" "$labelUnknown"
else
printDotText "groupID" "$fontGreen$groupId$fontReset"
fi
local ip
ip=$(systemHostGet "$domain")
if [[ -z "$ip" ]]; then
printDotText "/etc/hosts" "${fontGray}null$fontReset"
elif [[ "$ip" == '127.0.0.1' ]]; then
printDotText "/etc/hosts" "$fontGreen$ip$fontReset"
else
printDotText "/etc/hosts" "$fontRed$ip$fontReset"
fi
local limits blockLimit inodeLimit
if ! run limits error openlitespeedVhostQuotaGet "$ug"; then
printDotText "quota block limit" "$labelUnknown"
printDotText "quota inode limit" "$labelUnknown"
else
read -r blockLimit inodeLimit <<< "$limits"
blockLimit=$(numFormat "$blockLimit"000)
inodeLimit=$(numFormat "$inodeLimit")
printDotText "quota block limit" "$blockLimit"
printDotText "quota inode limit" "$inodeLimit"
fi
if groups "$ug" | grep -qw "$sftpAccessGroup"; then
printDotText "SFTP access" "$labelOn"
else
printDotText "SFTP access" "$labelOff"
fi
printSection "Config"
# printDotText "file$fontReset" "$appDataPath/config/$domain.config"
if [[ ! -f "$appDataPath/config/$domain.config" ]]; then
printDotText "file" "$fontRed$appDataPath/config/$domain.config$fontReset"
printDotText "file" "${fontRed}not found$fontReset"
else
printDotText "file" "$fontGreen$appDataPath/config/$domain.config$fontReset"
local -a params
local param value
params=(alias databaseName databaseUser databasePass redisUser redisPass postfixUser postfixPass postfixForwardTo sftpPass quotaBlockLimit quotaInodeLimit)
for param in "${params[@]}"; do
value=$(siteConfigGet "$domain" "$param")
printDotText "$param" "${value:-$labelNull}"
done
fi
printSection "OpenLiteSpeed"
if [[ ! -f "$olsVhostsConfigPath/$domain.conf" ]]; then
printDotText "file$fontReset" "$fontRed$olsVhostsConfigPath/$domain.conf$fontReset"
printDotText "file$fontReset" "${fontRed}not found$fontReset"
else
printDotText "file$fontReset" "$fontGreen$olsVhostsConfigPath/$domain.conf$fontReset"
fi
if [[ ! -d "$olsVhostsPath/$domain" ]]; then
printDotText "path$fontReset" "$fontRed$olsVhostsPath/$domain$fontReset"
printDotText "path$fontReset" "${fontRed}not found$fontReset"
else
printDotText "path$fontReset" "$fontGreen$olsVhostsPath/$domain$fontReset"
fi
if [[ ! -d "$olsPrivatePath/$domain" ]]; then
printDotText "data$fontReset" "$fontRed$olsPrivatePath/$domain$fontReset"
printDotText "data$fontReset" "${fontRed}not found$fontReset"
else
printDotText "data$fontReset" "$fontGreen$olsPrivatePath/$domain$fontReset"
fi
}
# Checks site resources (config, system, dirs, OLS, MariaDB, Redis, Postfix, HTTP).
# $1 (domain): site domain name.
# [$@] (opts): full|short (mode).
function cmdSiteStatus() {
local domain opt error section label note
domain=$(domainPrepare "$1")
if ! runError error domainCheck "$domain"; then
printDanger "$siteLabel $error"
return 1
fi
mode="${2:-full}"
printSection "Config"
if [[ -f "$appDataPath/config/$domain.config" ]]; then
printDotText "file" "$fontGreen$appDataPath/config/$domain.config$fontReset"
local -a params
local param value
params=(databaseName databaseUser databasePass redisUser redisPass postfixUser postfixPass quotaBlockLimit quotaInodeLimit)
for param in "${params[@]}"; do
value=$(siteConfigGet "$domain" "$param")
if [[ -n "$value" ]]; then
printDotText "$param" "$labelPass"
else
printDotText "$param" "$labelFail"
fi
done
else
printDotText "file" "$fontRed$appDataPath/config/$domain.config$fontReset"
fi
printSection "System"
local userId groupId ug
ug=$(domainToUser "$domain")
note="$fontGray$ug$fontReset"
if ! run userId error id -u "$ug"; then
printDotText "user" "$note $labelFail"
else
printDotText "user" "$note $labelPass"
fi
if ! run groupId error id -g "$ug"; then
printDotText "group" "$note $labelFail"
else
printDotText "group" "$note $labelPass"
fi
local ip
ip=$(systemHostGet "$domain")
note="$fontGray$ip$fontReset"
if [[ "$ip" != '127.0.0.1' ]]; then
printDotText "/etc/hosts" "$note $labelFail"
else
printDotText "/etc/hosts" "$note $labelPass"
fi
local limits blockLimit inodeLimit
if ! run limits error openlitespeedVhostQuotaGet "$ug"; then
printDotText "quota block limit" "$labelFail"
printDotText "quota inode limit" "$labelFail"
else
read -r blockLimit inodeLimit <<< "$limits"
blockLimit=$(numFormat "$blockLimit"000)
inodeLimit=$(numFormat "$inodeLimit")
printDotText "quota block limit" "$blockLimit $labelPass"
printDotText "quota inode limit" "$inodeLimit $labelPass"
fi
local sftpConfig
if ! sftpConfig=$(sshd -T -C user="$ug",host="$hostName",addr=127.0.0.1); then
printDotText "SFTP config" "$labelFail"
else
label="$fontBlue SFTP ForceCommand$fontReset"
if ! grep -Fxq "forcecommand internal-sftp -d /www -u 027" <<< "$sftpConfig"; then
printDotText "SFTP ForceCommand" "$labelFail"
else
printDotText "SFTP ForceCommand" "$labelPass"
fi
if ! grep -Fxq "chrootdirectory %h" <<< "$sftpConfig"; then
printDotText "SFTP ChrootDirectory" "$labelFail"
else
printDotText "SFTP ChrootDirectory" "$labelPass"
fi
fi
printSection "Path | existence, owner, permissions, ACL:nobody"
local path
path="$olsVhostsPath/$domain"
label="vhost $fontBlue/$fontReset"
note="${fontGray}755:root:root$fontReset"
if [[ ! -d "$path" ]]; then
printDotText "$label" "$note $labelFail"
elif ! fileSignatureCheck "$path" "755:root:root"; then
printDotText "$label" "$note $labelFail"
else
printDotText "$label" "$note $labelPass"
fi
path="$olsVhostsPath/$domain/www"
label="vhost $fontBlue/www$fontReset"
note="${fontGray}2750:u:g acl:r-x$fontReset"
if [[ ! -d "$path" ]]; then
printDotText "$label" "$note $labelFail"
elif ! fileSignatureCheck "$path" "2750:$ug:$ug"; then
printDotText "$label" "$note $labelFail"
elif ! fileSignatureAclCheck "$path" "user:nobody:r-x"; then
printDotText "$label" "$note $labelFail"
else
printDotText "$label" "$note $labelPass"
fi
local -a dirs
local dir
dirs=(session tmp)
for dir in "${dirs[@]}"; do
path="$olsVhostsPath/$domain/$dir"
label="vhost $fontBlue/$dir$fontReset"
note="${fontGray}770:u:g acl:rwx$fontReset"
if [[ ! -d "$path" ]]; then
printDotText "$label" "$note $labelFail"
elif ! fileSignatureCheck "$path" "770:$ug:$ug"; then
printDotText "$label" "$note $labelFail"
elif ! fileSignatureAclCheck "$path" "user:nobody:rwx"; then
printDotText "$label" "$note $labelFail"
else
printDotText "$label" "$note $labelPass"
fi
done
path="$olsPrivatePath/$domain"
label="vhost-data $fontBlue/$fontReset"
note="${fontGray}750:u:g acl:r-x$fontReset"
if [[ ! -d "$path" ]]; then
printDotText "$label" "$note $labelFail"
elif ! fileSignatureCheck "$path" "750:$ug:$ug"; then
printDotText "$label" "$note $labelFail"
elif ! fileSignatureAclCheck "$path" "user:nobody:r-x"; then
printDotText "$label" "$note $labelFail"
else
printDotText "$label" "$note $labelPass"
fi
path="$olsPrivatePath/$domain/bootstrap.php"
label="vhost-data $fontBlue/bootstrap.php$fontReset"
note="${fontGray}600:u:g acl:r--$fontReset"
if [[ ! -f "$path" ]]; then
printDotText "$label" "$note $labelFail"
elif ! fileSignatureCheck "$path" "600:$ug:$ug"; then
printDotText "$label" "$note $labelFail"
elif ! fileSignatureAclCheck "$path" "user:nobody:r--"; then
printDotText "$label" "$note $labelFail"
else
printDotText "$label" "$note $labelPass"
fi
# fileSignatureAclDiff "$path" "user:nobody:r--"
printSection "OpenLiteSpeed"
if ! run vhostList error openlitespeedConfigVhostList; then
printDotText "get vhost list" "$labelFail"
else
note="$fontGray$domain$fontReset"
if listContains "$domain" "$vhostList"; then
printDotText "vhost" "$note $labelPass"
else
printDotText "vhost" "$note $labelFail"
fi
fi
note="$fontGray$domain.conf$fontReset"
if [[ ! -f "$olsVhostsConfigPath/$domain.conf" ]]; then
printDotText "config" "$note $labelFail"
else
printDotText "config" "$note $labelPass"
fi
printSection "MariaDB"
local mariadbDatabaseList mariadbUserList
if ! run mariadbDatabaseList error mariadbDatabaseListGet; then
printDotText "get database list" "$labelFail"
elif ! run mariadbUserList error mariadbUserListGet; then
printDotText "get user list" "$labelFail"
else
mariadbDatabase=$(siteConfigGet "$domain" "databaseName")
note="$fontGray$mariadbDatabase$fontReset"
if ! listContains "$mariadbDatabase" "$mariadbDatabaseList"; then
printDotText "base" "$note $labelFail"
else
printDotText "base" "$note $labelPass"
fi
mariadbUser=$(siteConfigGet "$domain" "databaseUser")
note="$fontGray$mariadbUser$fontReset"
if ! listContains "$mariadbUser" "$mariadbUserList"; then
printDotText "user" "$note $labelFail"
else
printDotText "user" "$note $labelPass"
fi
if [[ "$mode" == "full" ]]; then
mariadbPass=$(siteConfigGet "$domain" "databasePass")
if ! run output error mariadbMasterExec mariadb -u"$mariadbUser" -p"$mariadbPass" -N -e "SELECT 1;"; then
printDotText "access" "$labelFail"
elif [[ "$output" != "1" ]]; then
printDotText "access" "$labelFail"
else
printDotText "access" "$labelPass"
fi
fi
fi
printSection "Redis"
local redisUserList
if ! run redisUserList error redisUserListGet; then
printDotText "$fontBlue get user list$fontReset" "$labelFail"
else
redisUser=$(siteConfigGet "$domain" "redisUser")
note="$fontGray$redisUser$fontReset"
if ! listContains "$redisUser" "$redisUserList"; then
printDotText "user" "$note $labelFail"
else
printDotText "user" "$note $labelPass"
fi
if [[ "$mode" == "full" ]]; then
redisPass=$(siteConfigGet "$domain" "redisPass")
if ! run output error redisExec redis-cli --no-auth-warning --user "$redisUser" --pass "$redisPass" PING; then
printDotText "access" "$labelFail"
elif [[ "$output" != "PONG" ]]; then
printDotText "access" "$labelFail"
else
printDotText "access" "$labelPass"
fi
fi
fi
printSection "Postfix"
local postfixSaslUserList
if ! run postfixSaslUserList error postfixSaslUserList; then
printDotText "get SASL list" "$labelFail"
else
postfixUser=$(siteConfigGet "$domain" "postfixUser")
note="$fontGray$postfixUser$fontReset"
if ! listContains "$postfixUser@$postfixDefaultRealm" "$postfixSaslUserList"; then
printDotText "SASL user" "$note $labelFail"
else
printDotText "SASL user" "$note $labelPass"
fi
postfixPass=$(siteConfigGet "$domain" "postfixPass")
printDotText "SASL access" "${fontGray}in progress$fontReset"
fi
if [[ "$mode" == "full" ]]; then
printSection "HTTP"
local httpCode urlEffective
if ! run httpCode error urlCode "http://$domain"; then
printDotText "HTTP code" "${fontGray}unknown$fontReset"
elif [[ "$httpCode" == 200 ]]; then
printDotText "HTTP code" "$fontGreen$httpCode$fontReset"
elif [[ "$httpCode" =~ ^[23][0-9]{2}$ ]]; then
printDotText "HTTP code" "$fontYellow$httpCode$fontReset"
else
printDotText "HTTP code" "$fontRed$httpCode$fontReset"
fi
if ! run urlEffective error urlAccessible "$domain"; then
printDotText "URL effective" "${fontGray}unknown$fontReset"
else
printDotText "URL effective" "$fontGreen$urlEffective$fontReset"
fi
printSection "Files"
siteFilesCheck "$domain"
fi
}
function siteFilesCheck() {
local domain error ug dots
domain=$(domainPrepare "$1")
if ! runError error domainCheck "$domain"; then
printDanger "$error"
return 1
fi
ug=$(domainToUser "$domain")
dots=30
# fileSignatureDiff "$olsVhostsPath/$domain" "755:root:root"
if ! run output error fileSignatureDiff "$olsVhostsPath/$domain" "755:root:root"; then
printDanger "${error:-$output}"
elif [[ -n "$output" ]]; then
local prefix=":$olsVhostsPath/$domain"
printDot "$dots" "${fontCyan}vhost d 755:root:root$fontReset" "" "${output/$prefix/ /}"
fi
# fileSignatureDiffList "$olsVhostsPath/$domain/www" "d" "2750|$ug:$ug"
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/www" "d" "2750|$ug:$ug"; then
printDanger "${error:-$output}"
else
lineCount=$(grep -c . <<< "$output" || true)
if [[ "$lineCount" -gt 0 ]]; then
local label="${fontCyan}www d 2750:u:g$fontReset"
local prefix=":$olsVhostsPath/$domain/www/"
while read -r line; do
printDot "$dots" "$label" "" "${line/$prefix/ /}"
done < <(awk 'NF' <<< "$output")
fi
fi
# fileSignatureDiffList "$olsVhostsPath/$domain/www" "f" "(600|640):$ug:$ug"
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/www" "f" "(600|640):$ug:$ug"; then
printDanger "${error:-$output}"
else
lineCount=$(grep -c . <<< "$output" || true)
if [[ "$lineCount" -gt 0 ]]; then
local label="${fontCyan}www f (600|640):u:g$fontReset"
local prefix=":$olsVhostsPath/$domain/www/"
while read -r line; do
printDot "$dots" "$label" "" "${line/$prefix/ /}"
done < <(awk 'NF' <<< "$output")
fi
fi
# fileSignatureDiffList "$olsVhostsPath/$domain/tmp" "d" "770:$ug:$ug"
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/tmp" "d" "770:$ug:$ug"; then
printDanger "${error:-$output}"
else
lineCount=$(grep -c . <<< "$output" || true)
if [[ "$lineCount" -gt 0 ]]; then
local label="${fontCyan}tmp d 770:u:g$fontReset"
local prefix=":$olsVhostsPath/$domain/tmp/"
while read -r line; do
printDot "$dots" "$label" "" "${line/$prefix/ /}"
done < <(awk 'NF' <<< "$output")
fi
fi
# fileSignatureDiffList "$olsVhostsPath/$domain/tmp" "f" "660:$ug:$ug"
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/tmp" "f" "660:$ug:$ug"; then
printDanger "${error:-$output}"
else
lineCount=$(grep -c . <<< "$output" || true)
if [[ "$lineCount" -gt 0 ]]; then
local label="${fontCyan}tmp f 660:u:g$fontReset"
local prefix=":$olsVhostsPath/$domain/tmp/"
while read -r line; do
printDot "$dots" "$label" "" "${line/$prefix/ /}"
done < <(awk 'NF' <<< "$output")
fi
fi
# fileSignatureDiffList "$olsVhostsPath/$domain/session" "d" "770:$ug:$ug"
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/session" "d" "770:$ug:$ug"; then
printDanger "${error:-$output}"
else
lineCount=$(grep -c . <<< "$output" || true)
if [[ "$lineCount" -gt 0 ]]; then
local label="${fontCyan}session d 770:u:g$fontReset"
local prefix=":$olsVhostsPath/$domain/session/"
while read -r line; do
printDot "$dots" "$label" "" "${line/$prefix/ /}"
done < <(awk 'NF' <<< "$output")
fi
fi
# fileSignatureDiffList "$olsVhostsPath/$domain/session" "f" "(660|600):$ug:$ug"
if ! run output error fileSignatureDiffList "$olsVhostsPath/$domain/session" "f" "(660|600):$ug:$ug"; then
printDanger "${error:-$output}"
else
lineCount=$(grep -c . <<< "$output" || true)
if [[ "$lineCount" -gt 0 ]]; then
local label="${fontCyan}session f (660|600):u:g$fontReset"
local prefix=":$olsVhostsPath/$domain/session/"
while read -r line; do
printDot "$dots" "$label" "" "${line/$prefix/ /}"
done < <(awk 'NF' <<< "$output")
fi
fi
# fileSignatureDiffList "$olsPrivatePath/$domain" "d" "750:$ug:$ug"
if ! run output error fileSignatureDiffList "$olsPrivatePath/$domain" "d" "750:$ug:$ug"; then
printDanger "${error:-$output}"
else
lineCount=$(grep -c . <<< "$output" || true)
if [[ "$lineCount" -gt 0 ]]; then
local label="${fontCyan}data d 750:u:g$fontReset"
local prefix=":$olsPrivatePath/$domain/"
while read -r line; do
printDot "$dots" "$label" "" "${line/$prefix/ /}"
done < <(awk 'NF' <<< "$output")
fi
fi
# fileSignatureDiffList "$olsPrivatePath/$domain" "f" "600:$ug:$ug"
if ! run output error fileSignatureDiffList "$olsPrivatePath/$domain" "f" "600:$ug:$ug"; then
printDanger "${error:-$output}"
else
lineCount=$(grep -c . <<< "$output" || true)
if [[ "$lineCount" -gt 0 ]]; then
local label="${fontCyan}data f 600:u:g$fontReset"
local prefix=":$olsPrivatePath/$domain/"
while read -r line; do
printDot "$dots" "$label" "" "${line/$prefix/ /}"
done < <(awk 'NF' <<< "$output")
fi
fi
}
+374
View File
@@ -0,0 +1,374 @@
storageLabel="[Storage]"
# Lists all backup directories on external storage with type labels (archive, daily, or unknown).
function cmdStorageBackupList() {
local dirList error dirCount archiveList dailyList
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
dirCount=$(grep -c . <<< "$dirList" || true)
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
printRow
local i=0 num dir label
while read -r dir; do
((++i))
num=$(printf "%0${#dirCount}d" "$i")
label="$num: $fontBlue$dir$fontReset"
if listContains "$dir" "$archiveList"; then
printDotText "$label" "${fontGreen}archive$fontReset"
elif listContains "$dir" "$dailyList"; then
printDotText "$label" "${fontGreen}daily$fontReset"
else
printDotText "$label" "$labelUnknown"
fi
done < <(awk 'NF' <<< "$dirList")
}
# Rotates archive backups on external storage: deletes old entries exceeding $storageArchiveKeep.
function cmdStorageBackupArchiveDispatch() {
local dirList error
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
printSection "Config"
printDotText "Type" "$storageType"
printDotText "Path" "$rsyncPath"
printDotText "Archive keep" "$storageArchiveKeep"
printDotText "Archive pattern" "$backupArchiveDirPattern"
printSection "Directories found"
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
local archiveCount archiveRemoveCount i=0 num label dirDate dirDays archiveRemoveList=""
archiveCount=$(grep -c . <<< "$archiveList" || true)
if [[ "$archiveCount" -gt 0 ]]; then
while read -r dir; do
((++i))
num=$(printf "%0${#archiveCount}d" "$i")
label="$num: $fontBlue$dir$fontReset"
if (( archiveCount - storageArchiveKeep >= i )); then
printDotText "$label" "${fontRed}delete$fontReset"
archiveRemoveList+=$'\n'"$dir"
else
printDotText "$label" "${fontGreen}save$fontReset"
fi
done < <(awk 'NF' <<< "$archiveList")
archiveRemoveCount=$(grep -c . <<< "$archiveRemoveList" || true)
if [[ "$archiveRemoveCount" -gt 0 ]]; then
printSection "Deleting Directories"
while read -r dir; do
label="$dir"
if ! runError error storageBackupRemove "$dir"; then
printDotText "$label" "$labelFail"
printDanger "$error"
else
printDotText "$label" "$labelDone"
fi
done < <(awk 'NF' <<< "$archiveRemoveList")
fi
fi
}
# Rotates daily backups on external storage: deletes old entries exceeding $storageDailyKeep, skips today.
function cmdStorageBackupDailyDispatch() {
local dirList error
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
printSection "Config"
printDotText "Type" "$storageType"
printDotText "Path" "$rsyncPath"
printDotText "Daily keep" "$storageDailyKeep"
printDotText "Daily pattern" "$backupDailyDirPattern"
printSection "Directories found"
dailyList=$(printf '%s\n' "$dirList" | grep -v "$appDate" | grep -E -- "$backupDailyDirPattern" | sort || true)
local dailyCount dailyRemoveCount i=0 num label dailyRemoveList=""
dailyCount=$(grep -c . <<< "$dailyList" || true)
if [[ "$dailyCount" -gt 0 ]]; then
while read -r dir; do
((++i))
num=$(printf "%0${#dailyCount}d" "$i")
label="$num: $fontBlue$dir$fontReset"
if [[ "$dir" == "$appDate" ]]; then
printDotText "$label" "${fontGray}skipped$fontReset"
elif (( dailyCount - storageDailyKeep >= i )); then
printDotText "$label" "${fontRed}delete$fontReset"
dailyRemoveList+=$'\n'"$dir"
else
printDotText "$label" "${fontGreen}save$fontReset"
fi
done < <(awk 'NF' <<< "$dailyList")
dailyRemoveCount=$(grep -c . <<< "$dailyRemoveList" || true)
if [[ "$dailyRemoveCount" -gt 0 ]]; then
printSection "Deleting Directories"
while read -r dir; do
label="$dir"
if ! runError error storageBackupRemove "$dir"; then
printDotText "$label" "$labelFail"
printDanger "$error"
else
printDotText "$label" "$labelDone"
fi
done < <(awk 'NF' <<< "$dailyRemoveList")
fi
fi
}
# Synchronizes the local path to external storage (rsync or SSH).
# $1 (sourcePath): local path to sync.
# [$2] (type): storage type (rsync or ssh); defaults to $storageType.
function cmdStoragePathSync() {
local sourcePath="$1"
[[ -n "$sourcePath" ]] || { printDanger "Source path not specified"; return 1; }
local type="${2:-$storageType}"
local error
printSection "Config"
printDotText "Source" "$sourcePath"
if [[ ! -e "$sourcePath" ]]; then
printDotText "$sourcePath" "${fontRed}not found$fontReset"
return 1
fi
case "$type" in
rsync)
printDotText "Type" "$type"
printDotText "rSync URI" "$rsyncUri"
printDotText "rSync path" "$rsyncPath"
printSection "Executing"
runError error storagePathSyncRsync "$sourcePath" || {
printDotText "synchronization" "$labelFail"
printDanger "$error"
return 1
}
;;
ssh)
printDotText "Type" "$type"
printDotText "SSH URI" "$sshUser@$sshHost"
printDotText "SSH path" "$sshPath"
printSection "Executing"
runError error storagePathSyncSSH "$sourcePath" || {
printDotText "synchronization" "$labelFail"
printDanger "$error"
return 1
}
;;
*)
printDotText "Type" "$type $labelUnknown"
return 1
;;
esac
printDotText "synchronization" "$labelDone"
}
# Syncs today's daily backup ($backupDailyPath/$appDate) to external storage.
function cmdStorageBackupDailySyncLast() {
cmdStoragePathSync "$backupDailyPath/$appDate"
}
# Syncs the newest archive backup directory to external storage.
function cmdStorageBackupArchiveSyncLast() {
local dirList archiveList archiveDir error
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
archiveDir=$(tail -n 1 <<< "$archiveList")
[[ -n "$archiveDir" ]] || { printDanger "No archive directories found"; return 1; }
cmdStoragePathSync "$backupArchivePath/$archiveDir"
}
# Interactively selects a local archive backup directory and syncs it to external storage.
function cmdStorageBackupArchiveSync() {
local dirList error archiveList archiveCount
run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; }
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
archiveCount=$(grep -c . <<< "$archiveList" || true)
printRow
local i=0 num dir
while read -r dir; do
((++i))
num=$(printf "%0${#archiveCount}d" "$i")
printDotText "$num: $fontBlue$dir$fontReset" "${fontGreen}archive$fontReset"
done < <(awk 'NF' <<< "$archiveList")
printRow
local input item selected
read -r -p "Specify the backup number: " input
printRow
[[ -z "$input" ]] && input=0
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid backup number"; return 1; }
item=$((10#$input))
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$archiveList")
[[ -n "$selected" ]] || { printDanger "Unknown backup number"; return 1; }
cmdStoragePathSync "$backupArchivePath/$selected"
}
# Interactively selects a local daily backup directory and syncs it to external storage.
function cmdStorageBackupDailySync() {
local dirList error dailyList dailyCount
run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; }
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
dailyCount=$(grep -c . <<< "$dailyList" || true)
printRow
local i=0 num dir
while read -r dir; do
((++i))
num=$(printf "%0${#dailyCount}d" "$i")
printDotText "$num: $fontBlue$dir$fontReset" "${fontGreen}daily$fontReset"
done < <(awk 'NF' <<< "$dailyList")
printRow
local input item selected
read -r -p "Specify the backup number: " input
printRow
[[ -z "$input" ]] && input=0
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid backup number"; return 1; }
item=$((10#$input))
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$dailyList")
[[ -n "$selected" ]] || { printDanger "Unknown backup number"; return 1; }
cmdStoragePathSync "$backupDailyPath/$selected"
}
# Dispatches interactive backup sync by type.
# $1 (type): backup type: archive or daily.
function cmdStorageBackupSync() {
local type
type="$1"
case "$type" in
archive)
cmdStorageBackupArchiveSync
;;
daily)
cmdStorageBackupDailySync
;;
*)
printDanger "Unknown type backup: $type";
return 1
;;
esac
}
# Interactively selects a backup directory on external storage and removes it.
function cmdStorageBackupRemove() {
local dirList error dirCount archiveList dailyList dir i=0 num label
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
dirCount=$(grep -c . <<< "$dirList" || true)
archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
printRow
while read -r dir; do
((++i))
num=$(printf "%0${#dirCount}d" "$i")
label="$num: $fontBlue$dir$fontReset"
if listContains "$dir" "$archiveList"; then
printDotText "$label" "${fontGreen}archive$fontReset"
elif listContains "$dir" "$dailyList"; then
printDotText "$label" "${fontGreen}daily$fontReset"
else
printDotText "$label" "$labelUnknown"
fi
done < <(awk 'NF' <<< "$dirList")
printRow
local input item selected
read -r -p "Specify the backup number: " input
printRow
[[ -z "$input" ]] && input=0
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid backup number"; return 1; }
item=$((10#$input))
selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$dirList")
[[ -n "$selected" ]] || { printDanger "Unknown backup number"; return 1; }
if ! runError error storageBackupRemove "$selected"; then
printDotText "$selected" "$labelFail"
printDanger "$error"
return 1
fi
printDotText "$selected" "$labelDone"
}
# Compares local and remote backup directories, showing which exist on each side.
function cmdStorageBackupCompare() {
local dirList error localArchiveList localDailyList remoteArchiveList remoteDailyList
run dirList error fileList "$backupArchivePath" d || { printDanger "Archive path: $error"; return 1; }
localArchiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
run dirList error fileList "$backupDailyPath" d || { printDanger "Archive path: $error"; return 1; }
localDailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
run dirList error storageFileList "/" d || { printDanger "$error"; return 1; }
remoteArchiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true)
remoteDailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true)
printSection "Local"
printDotText "Archive" "$(grep -c . <<< "$localArchiveList" || true)"
printDotText "Daily" "$(grep -c . <<< "$localDailyList" || true)"
printSection "Remote"
printDotText "Archive" "$(grep -c . <<< "$remoteArchiveList" || true)"
printDotText "Daily" "$(grep -c . <<< "$remoteDailyList" || true)"
local allDirs
allDirs=$(printf '%s\n' "$localArchiveList" "$localDailyList" "$remoteArchiveList" "$remoteDailyList" | awk 'NF' | sort -u)
printSection "Comparison"
local dir localStatus remoteStatus
out="${fontRed}X$fontReset"
localIn="${fontGreen}L$fontReset"
remoteIn="${fontGreen}R$fontReset"
while read -r dir; do
localStatus="$out"
remoteStatus="$out"
if listContains "$dir" "$localArchiveList"; then
localStatus="$localIn"
elif listContains "$dir" "$localDailyList"; then
localStatus="$localIn"
fi
if listContains "$dir" "$remoteArchiveList"; then
remoteStatus="$remoteIn"
elif listContains "$dir" "$remoteDailyList"; then
remoteStatus="$remoteIn"
fi
printDotText "$fontBlue$dir$fontReset" "[ $localStatus : $remoteStatus ]"
done < <(awk 'NF' <<< "$allDirs")
}
function cmdStorageBackupSize() {
local fileList file size total
printSection "FTP: $ftpPath"
total=0
fileList=$(ftpFileList | sort -n)
while IFS= read -r file; do
size=$(ftpPathSize "/$file" "gb")
printDotText "$file" "$size Gb"
(( total += size ))
done <<< "$fileList"
printRow
printDotText "total" "$total Gb"
}
+220
View File
@@ -0,0 +1,220 @@
systemLabel="[System]"
# Runs the full system installation flow.
function cmdInstallFull() {
systemApt || return 1
systemIpset || return 1
systemIptables || return 1
cmdK3sInstall || return 1
cmdK3sNamespaceAdd || return 1
cmdMariadbInstall || return 1
cmdRedisInstall || return 1
cmdOpenlitespeedInstall || return 1
cmdPostfixInstall || return 1
cmdWorkerInstall || return 1
cmdMetricInstall || return 1
}
# Displays numbered cron job list and stores selected job + current crontab in namerefs.
# Known jobs (cronJobs[]): green if installed, gray if missing.
# Unknown kube.sh jobs found in crontab: yellow, numbered after known jobs.
# $1 (varname): nameref for selected job string
# $2 (listvar): nameref for current crontab lines
function cmdCronSelect() {
local -n __cronJob="$1"
local -n __cronList="$2"
printRow
local error
run __cronList error systemCronList || { printDanger "Error retrieving the CRON job list: $error"; return 1; }
local -a shownJobs
local i job fontColor
for ((i=0; i<${#cronJobs[@]}; i++)); do
job="${cronJobs[$i]}"
shownJobs+=("$job")
grep -Fxq -- "$job" <<< "$__cronList" && fontColor="$fontGreen" || fontColor="$fontGray"
printf "%2d: %s\n" "$((i+1))" "$fontColor$job$fontReset"
done
while IFS= read -r job; do
[[ -z "$job" ]] && continue
grep -Fxq -- "$job" <(printf '%s\n' "${cronJobs[@]}") && continue
grep -Fq -- "$appPath/kube.sh" <<< "$job" || continue
shownJobs+=("$job")
printf "%2d: %s\n" "${#shownJobs[@]}" "$fontYellow$job$fontReset"
done <<< "$__cronList"
printRow
local input item
read -r -p "Specify the job number: " input
printRow
[[ -z "$input" ]] && input=0
[[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid job number"; return 1; }
item=$((10#$input - 1))
(( item < 0 || item >= ${#shownJobs[@]} )) && { printDanger "Unknown job number"; return 1; }
__cronJob="${shownJobs[$item]}"
}
# Interactively selects and adds a managed cron job to root crontab.
function cmdCronAdd() {
local selected jobList
cmdCronSelect selected jobList || return 1
printDotText "job" "$selected"
grep -Fxq -- "$selected" <(printf '%s\n' "${cronJobs[@]}") || {
printDotText "adding" "$labelFail"
printDanger "Cannot add unmanaged job"
return 1
}
if grep -Fxq -- "$selected" <<< "$jobList"; then
printDotText "adding" "$labelDone"
return 0
fi
local tmp
tmp=$(mktemp) || return 1
{ printf '%s\n' "$jobList"; printf '%s\n' "$selected"; } > "$tmp"
crontab -u root "$tmp" || {
rm -f "$tmp"
printDotText "adding" "$labelFail"
return 1
}
rm -f "$tmp"
printDotText "adding" "$labelDone"
}
# Interactively selects and removes a managed cron job from root crontab.
function cmdCronRemove() {
local selected jobList
cmdCronSelect selected jobList || return 1
printDotText "job" "$selected"
if ! grep -Fxq -- "$selected" <<< "$jobList"; then
printDotText "removing" "$labelDone"
return 0
fi
local tmp
tmp=$(mktemp) || return 1
grep -Fxv -- "$selected" <<< "$jobList" > "$tmp"
crontab -u root "$tmp" || {
rm -f "$tmp"
printDotText "removing" "$labelFail"
return 1
}
rm -f "$tmp"
printDotText "removing" "$labelDone"
}
# Shows managed cron jobs; installed entries in green, missing in gray, unknown in yellow.
function cmdCronList() {
local jobList error job fontColor list=""
run jobList error systemCronList || {
printDanger "systemCronList: $error";
return 1;
}
for ((i=0; i<${#cronJobs[@]}; i++)); do
grep -Fxq -- "${cronJobs[$i]}" <<< "$jobList" && fontColor="$fontGreen" || fontColor="$fontGray"
list+=$'\n'"$fontColor${cronJobs[$i]}$fontReset"
done
while IFS= read -r job; do
[[ -z "$job" ]] && continue
grep -Fxq -- "$job" <(printf '%s\n' "${cronJobs[@]}") && continue
grep -Fq -- "$appPath/kube.sh" <<< "$job" || continue
list+=$'\n'"$fontYellow$job$fontReset"
done <<< "$jobList"
printRow
local i=0 line total
total=$(grep -c . <<< "$list")
while IFS= read -r line; do
[[ -n "$line" ]] || continue
((i++))
num=$(printf "%${#total}d" "$i")
printDotFix 20 "$num: $line"
done <<< "$list"
}
# Lists users in the SFTP access group.
function cmdSftpUserList() {
local output error
printRow
if ! run output error sftpUserList "$@"; then
printDanger "$error"
else
printText "$output"
fi
}
# Enables SFTP access for a domain user.
function cmdSftpAccessEnable() {
local error
printRow
if ! runError error sftpAccessEnable "$@"; then
printDotText "SFTP access enable" "$labelFail"
printDanger "$error"
else
printDotText "SFTP access enable" "$labelDone"
fi
}
# Disables SFTP access for a domain user by removing them from the SFTP group.
function cmdSftpAccessDisable() {
local error
printRow
if ! runError error sftpAccessDisable "$@"; then
printDotText "SFTP access enable" "$labelFail"
printDanger "$error"
else
printDotText "SFTP access enable" "$labelDone"
fi
}
# Sets the SFTP password for a domain user from site config.
function cmdSftpPasswordSet() {
local error
printRow
if ! runError error sftpPasswordSet "$@"; then
printDotText "SFTP password set" "$labelFail"
printDanger "$error"
else
printDotText "SFTP password set" "$labelDone"
fi
}
# [WARNING] Patches sshd_config to enable SFTP via internal-sftp with group-based chroot.
function cmdSftpAddingSupport() {
local error
printSection "Add SFTP support"
if ! runError error sftpAddingSupport; then
printDotText "adding" "$labelFail"
printDanger "$error"
else
printDotText "adding" "$labelDone"
fi
printSection "Update fail2ban config"
if ! runError error fail2banConfigUpdate; then
printDotText "updating" "$labelFail"
printDanger "$error"
else
printDotText "updating" "$labelDone"
fi
}
+215
View File
@@ -0,0 +1,215 @@
testLabel="[Test]"
# Detect current master pod by parsing INFO replication.
function redisReplicaMasterGet() {
local output error
if ! run output error k3sPodList "$redisKube"; then
appError "$error"
return 1
fi
while read -r pod; do
if ! run output error redisPodExecCli "$pod" INFO replication; then
appError "$pod | $error"
continue
fi
if echo "$output" | grep -q "role:master"; then
echo "$pod"
return 0
fi
done < <(awk 'NF' <<<"$output")
appError "Master node not found"
return 1
}
# Test MariaDB replica
function testMariadbReplica() {
local database=$(uuidgen)
if ! run output error mariadbMasterRootQuery "CREATE DATABASE \`$database\`;"; then
printDanger "$testLabel $mariadbMasterKube | Database: $database | Create: $error"
return 1
else
printSuccess "$testLabel $mariadbMasterKube | Database: $database | Create: OK"
fi
local databaseMaster
if ! run databaseMaster error mariadbMasterRootQuery "SHOW DATABASES LIKE '$database';"; then
printDanger "$testLabel "$mariadbMasterKube" | Database list: $error"
else
if [[ "$databaseMaster" == "$database" ]]; then
printSuccess "$testLabel $mariadbMasterKube | Database: $database | Exists"
else
printDanger "$testLabel $mariadbMasterKube | Database: $database | Not found"
fi
fi
local databaseSlave
if ! run databaseSlave error mariadbSlaveRootQuery "SHOW DATABASES LIKE '$database';"; then
printDanger "$testLabel "$mariadbSlaveKube" | Database list: $error"
else
if [[ "$databaseSlave" == "$database" ]]; then
printSuccess "$testLabel $mariadbSlaveKube | Database: $database | Exists"
else
printDanger "$testLabel $mariadbSlaveKube | Database: $database | Not found"
fi
fi
if ! run output error mariadbMasterRootQuery "DROP DATABASE \`$database\`;"; then
printDanger "$testLabel $mariadbMasterKube | Database: $database | Drop: $error"
return 1
else
printSuccess "$testLabel $mariadbMasterKube | Database: $database | Drop: OK"
fi
}
# Test Redis cluster
function testRedisCluster() {
local key=$(uuidgen)
local value=$(uuidgen)
local podMaster podList error
if ! run podList error k3sPodList "$redisKube"; then
printDanger "$testLabel k3sPodList: $error"
return 1
fi
if ! run podMaster error redisReplicaMasterGet; then
printDanger "$testLabel redisReplicaMasterGet: $error"
return 1
fi
if ! run output error redisPodExecCli "$podMaster" SET "$key" "$value"; then
printDanger "$testLabel $podMaster | Key: $key | Set: $error"
return 1
else
printSuccess "$testLabel $podMaster | Key: $key | Set: $value"
fi
while read pod; do
if ! run output error redisPodExecCli "$pod" GET "$key"; then
printDanger "$testLabel $pod | Key: $key | Get: $error"
continue
fi
if [[ "$output" == "$value" ]]; then
printSuccess "$testLabel $pod | Key: $key | Get: $output"
else
printDanger "$testLabel $pod | Key: $key | Get: $output"
fi
done < <(awk 'NF' <<<"$podList")
if ! run output error redisPodExecCli "$podMaster" DEL "$key"; then
printDanger "$testLabel $podMaster | Key: $key | Del: $error"
return 1
else
printSuccess "$testLabel $podMaster | Key: $key | Del: OK"
fi
}
# Test create site
function testSite() {
local domain
domain="test-$(stringRandom 16 'a-z').test"
if ! run output error cmdSiteAddDefault "$domain"; then
printDanger "$testLabel Domain: $domain | Create: $error"
else
printSuccess "$testLabel Domain: $domain | Create: OK"
cmdOpenlitespeedRestart
if ! run output error systemHostAdd "$domain"; then
printDanger "$testLabel Domain: $domain | Host add: $error"
else
printSuccess "$testLabel Domain: $domain | Host add: OK"
fi
local code
if ! run code error curl -s -L -o /dev/null -w "%{http_code}" "$domain"; then
printDanger "$testLabel Domain: $domain | Curl: $code: $error"
else
if [[ "$code" == "200" ]]; then
printSuccess "$testLabel Domain: $domain | Curl: $code"
else
printWarning "$testLabel Domain: $domain | Curl: $code"
fi
fi
if ! run output error systemHostRemove "$domain"; then
printDanger "$testLabel Domain: $domain | Host remove: $error"
else
printSuccess "$testLabel Domain: $domain | Host remove: OK"
fi
fi
if ! run output error cmdSiteRemove "$domain"; then
printDanger "$testLabel Domain: $domain | Site remove: $error"
else
printSuccess "$testLabel Domain: $domain | Site remove: OK"
fi
cmdOpenlitespeedRestart
}
# Test create site Wordpress
function testSiteWordpress() {
local domain
domain="test-$(stringRandom 16 'a-z').test"
if ! run output error cmdSiteAddWordpress $domain; then
printDanger "$testLabel Domain: $domain | Create: $error"
else
printSuccess "$testLabel Domain: $domain | Create: OK"
cmdOpenlitespeedRestart
if ! run output error systemHostAdd "$domain"; then
printDanger "$testLabel Domain: $domain | Host add: $error"
else
printSuccess "$testLabel Domain: $domain | Host add: OK"
fi
local code
if ! run code error curl -s -L -o /dev/null -w "%{http_code}" "$domain"; then
printDanger "$testLabel Domain: $domain | Curl: $code: $error"
else
if [[ "$code" == "200" ]]; then
printSuccess "$testLabel Domain: $domain | Curl: $code"
else
printWarning "$testLabel Domain: $domain | Curl: $code"
fi
fi
if ! run output error systemHostRemove "$domain"; then
printDanger "$testLabel Domain: $domain | Host remove: $error"
else
printSuccess "$testLabel Domain: $domain | Host remove: OK"
fi
fi
if ! run output error cmdSiteRemove "$domain"; then
printDanger "$testLabel Domain: $domain | Site remove: $error"
else
printSuccess "$testLabel Domain: $domain | Site remove: OK"
fi
cmdOpenlitespeedRestart
}
# testRedisCluster
# local key="kube:haproxy:uuid" uuid
# uuid=$(uuidgen)
# if run output error redisExecCli -h redis-master -p 6379 SET "$key" "$uuid"; then
# printInfo "$redisHaproxyLabel Set $key | $uuid"
# else
# printDanger "$redisHaproxyLabel Set $key: $error"
# fi
# if run output error redisExecCli -h redis-master -p 6379 GET "$key"; then
# printInfo "$redisHaproxyLabel Get $key | $output"
# if [[ "$uuid" != "$output" ]]; then
# printDanger "$redisHaproxyLabel Validation failed"
# else
# printSuccess "$redisHaproxyLabel Validation success"
# fi
# else
# printDanger "$redisHaproxyLabel Get $key: $error"
# fi
+59
View File
@@ -0,0 +1,59 @@
UNIGMA_PHP=""
UNIGMA_MEM=""
UNIGMA_EXEC=""
function cmdUnigma() {
local podList vhostsList error raw pod phase
printSection "Unigma"
run podList error k3sPodListStatus "$olsKube" || { printDanger "Get pods: $error"; return 1; }
[[ -n "$podList" ]] || { printDanger "Pods not found"; return 1; }
# run vhostList error fileList "$olsVhostsPath" d || { printDanger "$error"; return 1; }
run vhostList error openlitespeedConfigVhostList || { printDanger "Failed get list of vhosts: $error"; return 1; }
while read -r vhost; do
run raw error unigmaGetTxt "$vhost" || {
printDotText "$vhost" "${fontGray}failed get Unigma data$fontReset";
continue
}
# raw='php=8.1;mem=112M;exec=60'
unigmaParse "$raw" || {
printDotText "$vhost" "$labelFail";
printDanger "$error"
continue
}
unigmaIniSet "$vhost" "$UNIGMA_MEM" "$UNIGMA_EXEC"
case $? in
1) printDotText "$vhost" "$labelFail"; printDanger "$error"; continue ;;
2)
uid=$(domainToUid "$vhost")
[[ -n "$uid" ]] || {
printDotText "$vhost" "$labelFail";
printDanger "Cannot resolve UID for: $vhost";
continue;
}
local pod phase
while IFS='|' read -r pod phase; do
if [[ "$phase" != "Running" ]]; then
printDotText " $pod" "$fontRed$phase$fontReset"
else
runSilent openlitespeedPodExec "$pod" pkill -u "$uid" -x lsphp
printDotText " kill$fontBlue lsphp$fontReset processes for $vhost"
fi
done < <(awk 'NF' <<< "$podList")
;;
esac
unigmaPhpSet "$vhost" "$UNIGMA_PHP" || {
printDotText "$vhost" "$labelFail";
printDanger "$error"
continue
}
printDotText "$vhost" "$labelDone";
done < <(awk 'NF' <<< "$vhostList")
}
+238
View File
@@ -0,0 +1,238 @@
wordpressLabel="[Wordpress]"
# Lists all WordPress users for a site.
# $1 (domain): site domain name.
function cmdWordpressUserList() {
local output error
printRow
if ! run output error wordpressUserList "$@"; then
printDanger "$error"
else
printText "$output"
fi
}
# Sets the password for a WordPress user.
# $1 (domain): site domain name.
# $2 (user): WordPress username or user ID.
# $3 (password): new password.
function cmdWordpressPasswordSet() {
local output error
printRow
if ! run output error wordpressPasswordSet "$@"; then
printDanger "$error"
else
printText "$output"
fi
}
# Executes an arbitrary WP-CLI command inside the site's vhost or all vhosts.
# $1 (all|domain): site domain name or all vhosts.
# $@ (...): WP-CLI sub-command and arguments.
function cmdWordpressExec() {
local target="$1"
shift || true
local vhostList error
printRow
if [[ -z "$target" ]]; then
printDanger "Target not specified";
elif ! run vhostList error openlitespeedConfigVhostList; then
printDanger "Cannot get vhost list: $error";
elif [[ "$target" == "all" ]]; then
local domain
for domain in $vhostList; do
printSection "$domain"
if ! run output error wordpressExec "$domain" "$@"; then
printDanger "$error"
else
printText "$output"
fi
done
elif ! listContains "$target" "$vhostList"; then
printDanger "Unknown domain: $target";
elif ! run output error wordpressExec "$target" "$@"; then
printDanger "$error"
else
printText "$output"
fi
}
function cmdWordpressSalt() {
local target="$1"
local vhostList error
printRow
if [[ -z "$target" ]]; then
printDanger "Target not specified";
elif ! run vhostList error openlitespeedConfigVhostList; then
printDanger "Cannot get vhost list: $error";
elif [[ "$target" == "all" ]]; then
local domain
for domain in $vhostList; do
if ! runError error wordpressSalt "$domain"; then
printDotText "$domain" "$labelFail"
printDanger "$error"
else
printDotText "$domain" "$labelDone"
fi
done
elif ! listContains "$target" "$vhostList"; then
printDanger "Unknown domain: $target";
elif ! runError error wordpressSalt "$target"; then
printDotText "$target" "$labelFail"
printDanger "$error"
else
printDotText "$target" "$labelDone"
fi
}
function cmdWordpressCheck() {
local target="$1"
local vhostList error
printRow
if [[ -z "$target" ]]; then
printDanger "Target not specified";
elif ! run vhostList error openlitespeedConfigVhostList; then
printDanger "Cannot get vhost list: $error";
elif [[ "$target" == "all" ]]; then
local domain
for domain in $vhostList; do
printSection "$domain | core"
wordpressExec "$domain" core verify-checksums
printSection "$domain | plugins"
wordpressExec "$domain" plugin verify-checksums --all
done
elif ! listContains "$target" "$vhostList"; then
printDanger "Unknown domain: $target";
else
printSection "$target | core"
wordpressExec "$target" core verify-checksums
printSection "$target | plugins"
wordpressExec "$target" plugin verify-checksums --all
fi
}
# 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 cmdWordpressDomainUpdate() {
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 --> $olsPodVhostsPath/$domain/www"
wordpressSearchReplace "$domain" "$abspathOld" "$olsPodVhostsPath/$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
}
+301
View File
@@ -0,0 +1,301 @@
workerLabel="[Worker]"
taskStatusExecution="execution"
taskStatusSuccess="success"
taskStatusFailed="failed"
taskStatusWaiting="waiting"
taskStatusNew="new"
# Prepares worker agent directory and UUID file.
function cmdWorkerPreparation() {
printSection "Preparation..."
if [[ ! -f "$workerAgentPath/worker.py" ]]; then
mkdir -p "$workerAgentPath"
cp -f -- "$appAssetsPath/$workerKube/worker.py" "$workerAgentPath/worker.py"
fi
rm -f "$workerAgentPath/worker.uuid"
fileValueGetOrCreate "$workerAgentPath/worker.uuid" "$hostUuid" >/dev/null 2>&1 || {
printDotText "preparation" "$labelFail"
printDanger "Generation UUID failed";
return 1;
}
printDotText "preparation" "$labelDone"
}
# Renders the Worker Kubernetes YAML manifest via Helm.
function cmdWorkerYamlRender() {
local templateFile="templates/$workerKube.yaml"
printSection "Render YAML file | Helm"
printDotText "Template" "$appAssetsPath/k3s/$templateFile"
[[ -f "$appAssetsPath/k3s/$templateFile" ]] || {
printDanger "Template file not found"
return 1
}
local varsFile
varsFile=$(mktemp "/tmp/$workerKube.vars.XXXXXX.yaml") || {
printDotText "render" "$labelFail"
printDanger "Create temp variables file"
return 1
}
printDotText "Variables" "$varsFile"
trap 'rm -f -- "$varsFile"' RETURN
cat > "$varsFile" <<EOF
workerHelm: true
namespace: $k3sNamespace
worker: $workerKube
workerAgentPath: $workerAgentPath
workerTasksPath: $workerTasksPath
workerUuid: $hostUuid
workerName: $workerName
workerPool: $workerPool
workerApiUrl: $workerApiUrl
workerApiGettingPause: $workerApiGettingPause
workerApiSendingPause: $workerApiSendingPause
EOF
printDotText "Result" "$workerYaml"
mkdir -p -- "$(dirname -- "$workerYaml")" || return 1
fileBackup "$workerYaml"
helm template stack "$appAssetsPath/k3s" -f "$varsFile" --show-only "$templateFile" > "$workerYaml" || {
printDotText "render" "$labelFail"
printDanger "Render failed"
return 1
}
printDotText "render" "$labelDone"
}
function cmdWorkerUpdate() {
cmdWorkerYamlRender || return 1
cmdK3sYamlApplyEx "$workerYaml" || return 1
}
# Installs Worker into Kubernetes.
function cmdWorkerInstall() {
cmdWorkerPreparation || return 1
cmdWorkerYamlRender || return 1
cmdK3sYamlApplyEx "$workerYaml" || return 1
}
# Uninstalls Worker Kubernetes resources.
function cmdWorkerUninstall() {
"$k3sCmd" kubectl delete -f "$workerYaml"
}
# Executes one worker task described in an INI-like config file.
# $1 (taskFile): path to the task config file.
# [$2] (domain): override domain (read from task file if omitted).
function cmdWorkerTaskHandle() {
local taskFile="$1"
[[ -n "$taskFile" ]] || { printDanger "$workerLabel Task file not specified"; return 1; }
[[ -f "$taskFile" ]] || { printDanger "$workerLabel Task: $taskFile | File not found"; return 1; }
printInfo "$workerLabel Task: $taskFile"
local domain="$2"
[[ -n "$domain" ]] || domain=$(configGet "$taskFile" "domain")
local status
status=$(configGet "$taskFile" "status")
if [[ "$status" != "$taskStatusNew" && "$status" != "$taskStatusWaiting" ]]; then
printDanger "$workerLabel Task: $taskFile | Status not '$taskStatusNew' or '$taskStatusWaiting'"
return 1
fi
configSet "$taskFile" "status" "$taskStatusExecution"
configSet "$taskFile" "start" "$(date +%s)"
local action
action=$(configGet "$taskFile" "action")
printInfo "$workerLabel Action: $action"
case "$action" in
"copy")
local databaseUrl
databaseUrl=$(configGet "$taskFile" "database_url")
if ! run output error urlAccessible "$databaseUrl"; then
printDanger "$workerLabel URL database archive is invalid: $databaseUrl"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "URL database archive is invalid: $databaseUrl"
return 1
fi
local filesUrl
filesUrl=$(configGet "$taskFile" "files_url")
if ! run output error urlAccessible "$filesUrl"; then
printDanger "$workerLabel URL files archive is invalid: $filesUrl"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "URL files archive is invalid: $filesUrl"
return 1
fi
if [[ -z "$domain" ]]; then
domain=$(domainsListMark "$domainsList")
fi
if [[ -z "$domain" ]]; then
printDanger "$workerLabel Domain not specified or no domains available"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "Domain not specified or no domains available"
return 1
else
configSet "$taskFile" "domain" "$domain"
fi
configSet "$taskFile" "status" "$taskStatusExecution"
mkdir -p "$workerFilesPath"
local fileName filesLocal databaseLocal
fileName="${domain}_$(uuidgen)"
databaseLocal="$workerFilesPath/$(urlLocalFileGen "$databaseUrl" "$fileName")"
printInfo "$workerLabel Download archive database --> $databaseLocal"
if ! run output error fileDownload "$databaseUrl" "$databaseLocal"; then
printDanger "$error"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "$error"
return 1
fi
filesLocal="$workerFilesPath/$(urlLocalFileGen "$filesUrl" "$fileName")"
printInfo "$workerLabel Download archive files --> $filesLocal"
if ! run output error fileDownload "$filesUrl" "$filesLocal"; then
printDanger "$error"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "$error"
return 1
fi
printInfo "$workerLabel Create site: $domain"
if ! run output error cmdSiteAddWordpress "$domain" "$filesLocal" "$databaseLocal"; then
printDanger "$error"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "Error create site: $error"
return 1
else
cmdWordpressDomainUpdate "$domain"
cmdOpenlitespeedRestart
fi
;;
"pack")
local output error
local uuid="worker_$(uuidgen)"
if ! run vhostList error openlitespeedConfigVhostList; then
printDanger "Vhost list: $error"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "Error getting list of virtual hosts"
return 1
elif ! listContains "$domain" "$vhostList"; then
printDanger "Vhost list: Domain is not exists in OpenLiteSpeed config"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "Domain is not exists in OpenLiteSpeed config"
return 1
fi
if ! run output error backupSiteFiles "$domain" "$olsVhostsPath/$domain/www/$uuid"; then
printDanger "$error"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "Error create files archive: $error"
return 1
fi
if ! run output error backupSiteDatabase "$domain" "$olsVhostsPath/$domain/www/$uuid.sql"; then
printDanger "$error"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "Error create database archive: $error"
return 1
fi
configSet "$taskFile" "files_url" "https://$domain/$uuid.tar.gz"
configSet "$taskFile" "database_url" "https://$domain/$uuid.sql.tar.gz"
;;
"delete")
printSuccess "$workerLabel Action: Site delete..."
cmdSiteRemove "$domain"
cmdOpenlitespeedRestart
;;
"update")
domain=$(configGet "$taskFile" "domain")
if [[ -z "$domain" ]]; then
printDanger "$workerLabel Domain not specified"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "Domain not specified"
return 1
fi
local domainList
domainList=$(postfixDomainList)
if ! listContains "$domain" "$domainList"; then
printDanger "$workerLabel Domain $domain not found"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "Domain $domain not found"
return 1
fi
local postfixForwardTo
postfixForwardTo=$(configGet "$taskFile" "mail_forward_to")
siteConfigSet "$domain" "postfixForwardTo" "$postfixForwardTo"
postfixVirtualAliasSet "@$domain" "$postfixForwardTo"
postfixPostmapRebuild
;;
*)
printDanger "$workerLabel Unknown action: $action"
configSet "$taskFile" "status" "$taskStatusFailed"
configSet "$taskFile" "message" "Unknown action: $action"
return 1
;;
esac
configSet "$taskFile" "status" "$taskStatusSuccess"
printSuccess "$workerLabel Action: $action | Success"
local taskFileBase
taskFileBase=$(basename -- "$taskFile")
mkdir -p "$appDataPath/worker-task" || true
cp -f -- "$taskFile" "$appDataPath/worker-task/$taskFileBase" 2>/dev/null || true
}
# Scans task dir and runs handler for tasks with status new or waiting.
function cmdWorkerObserver() {
local fileList error
run fileList error fileList "$workerTasksPath" f || { appError "$error"; return 1; }
while IFS= read -r file; do
local taskFile="$workerTasksPath/$file"
local status
status=$(configGet "$taskFile" "status")
if [[ "$status" == "$taskStatusNew" || "$status" == "$taskStatusWaiting" ]]; then
printSuccess "$workerLabel $file | Status: $status | Starting..."
cmdWorkerTaskHandle "$taskFile"
else
printInfo "$workerLabel $file | Status: $status | Skip"
fi
done < <(awk 'NF' <<< "$fileList")
}
# Lists worker tasks with action, status, and lifetime in seconds.
function cmdWorkerTaskList() {
local fileList error
run fileList error fileList "$workerTasksPath" f || {
printDanger "$error";
return 1;
}
local count=0
while IFS= read -r file; do
((count++))
local task=${file%.task}
local action status start
action=$(configGet "$workerTasksPath/$file" "action")
status=$(configGet "$workerTasksPath/$file" "status")
start=$(configGet "$workerTasksPath/$file" "start")
local live="?"
if [[ -n "$start" ]]; then
live=$(( $(date +%s) - start ))
fi
printSection "$task"
printDotText "file" "$file"
printDotText "action" "$action"
printDotText "status" "$status"
printDotText "live, sec" "$live"
done < <(awk 'NF' <<< "$fileList")
}
+726
View File
@@ -0,0 +1,726 @@
# Searches for entries (files and directories) in the specified local directory.
#
# $1 (path): The path to the directory on the local machine where the search for entries will be performed.
# $2 (type): Type entry (f: files, d: directories, ...).
function fileList() {
local path="${1-}"
local type="${2-}"
local args=(-mindepth 1 -maxdepth 1)
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
[[ -d "$path" ]] || { appError "Directory not found: $path"; return 1; }
if [[ -n "$type" ]]; then
case "$type" in
f|d|l|p|s|b|c) args+=(-type "$type") ;;
*) appError "Unsupported file type: $type"; return 1 ;;
esac
fi
find "$path" "${args[@]}" -printf '%f\n'
}
# Searches for entries (files or directories) in the specified directory on an FTP server.
#
# $1 (path): The path to the directory on the FTP server where the search for entries will be performed.
# $2 (type): Type entry (f: files, d: directories, ...).
function ftpFileList() {
local path="$1"
local type="$2"
local output error
run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$path/" --connect-timeout 10 \
|| { appError "$error"; return 1; }
case "$type" in
f) printf '%s\n' "$output" | grep -v '^d' | awk '{print $NF}' ;;
d) printf '%s\n' "$output" | grep '^d' | awk '{print $NF}' ;;
*) printf '%s\n' "$output" | awk '{print $NF}' ;;
esac
}
# Searches for entries (files or directories) in the specified directory on a remote server via SSH.
#
# $1 (path): The path to the directory on the remote server where the search for entries will be performed.
# $2 (type): Type entry (f: files, d: directories, ...).
function sshFileList() {
local path="$1"
local type="$2"
local typeArg="${type:+-type $type}"
local output error
run output error ssh -i "$sshKeyFile" "$sshUser@$sshHost" \
"find '${sshPath}${path}' -maxdepth 1 -mindepth 1 ${typeArg} -exec basename {} \\;" \
|| { appError "$error"; return 1; }
printf '%s\n' "$output"
}
# Searches for entries (files or directories) in the specified directory on external storage.
function storageFileList() {
local -A storageFunc=(
[ftp]=ftpFileList
[rsync]=ftpFileList
[ssh]=sshFileList
)
[[ -n "${storageFunc[$storageType]:-}" ]] || { appError "Unknown value storageType: $storageType"; return 1; }
"${storageFunc[$storageType]}" "$@"
}
# Cleans a specified directory on a remote server using rsync.
#
# $1 (path): The path to the directory to be cleaned on the remote server.
#
# The function creates a temporary empty directory on the local machine and syncs it with the remote directory
# using rsync with the `--delete` option, which removes any files on the remote side that are not present in
# the local directory. After completion, the temporary directory is deleted. An error message is displayed in case of failure.
function rsyncDirectoryClean() {
local path="$1"
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
local emptyPath="$appDataPath/$(uuidgen)"
local error
runError error mkdir -p "$emptyPath" || { appError "Failed to create temp directory: $error"; return 1; }
runError error rsync -r --delete --password-file="$rsyncPassFile" "$emptyPath/" rsync://"$rsyncUri$path/" \
|| { rm -rf "$emptyPath"; appError "Failed to clean remote directory: $error"; return 1; }
rm -rf "$emptyPath"
}
# Deletes a directory on an FTP server.
#
# $1 (path): The path to the directory on the FTP server to be deleted (in ftpPath).
function ftpDirectoryDelete() {
local path="$1"
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
local error
runError error curl -u "$ftpUser:$ftpPass" -Q "-RMD $ftpPath$path" "ftp://$ftpHost:$ftpPort" --connect-timeout 10 \
|| { appError "$error"; return 1; }
}
# Deletes a directory on a remote server via SSH (in sshPath).
#
# $1 (path): The path to the directory to be deleted.
function sshDirectoryDelete() {
local path="$1"
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
local error
runError error ssh -i "$sshKeyFile" "$sshUser@$sshHost" "rm -rf '${sshPath}${path}'" \
|| { appError "$error"; return 1; }
}
function fileAtomicWrite() {
local file="$1"
local content="$2"
local path tmp
path="$(dirname "$file")"
mkdir -p "$path"
tmp="$(mktemp "$path/.tmp.XXXXXX")"
printf '%s\n' "$content" > "$tmp"
mv -f "$tmp" "$file"
}
# Create a timestamped backup copy of a file if it exists.
# $1 (file): path to the file to back up.
# [$2] (suffix): custom suffix for the backup file (defaults to a timestamp .bak suffix).
function fileBackup() {
local file="$1"
if [[ -z "$file" ]]; then
appError "File not specified"
return 1
fi
if [[ ! -f "$file" ]]; then
return 0
fi
local suffix="${2:-.$(date +%F_%H-%M-%S).bak}"
cp -p -- "$file" "$file$suffix" || {
appError "Failed to backup file: $file"
return 1
}
return 0
}
# Download remote file to a local path.
function fileDownload() {
local remoteFile="$1"
local localFile="$2"
local retries="${3:-5}"
local delay="${4:-3}"
[[ -n "$remoteFile" ]] || { appError "Remote file not specified"; return 1; }
[[ -n "$localFile" ]] || { appError "Local file not specified"; return 1; }
mkdir -p "$(dirname "$localFile")" || { appError "Failed to create folder"; return 1; }
local tmpFile="${localFile}.part"
local i rc curlError lastError
for ((i = 1; i <= retries; i++)); do
curlError=$(curl -kfsSL --http1.1 --max-redirs 10 --connect-timeout 30 --speed-time 60 --speed-limit 1024 -C - -o "$tmpFile" "$remoteFile" 2>&1)
rc=$?
if [[ $rc -eq 0 ]]; then
mv -f "$tmpFile" "$localFile" || { appError "Failed to move downloaded file"; return 1; }
return 0
fi
lastError="$curlError"
[[ $rc -ne 33 ]] || rm -f "$tmpFile" # rc=33: server doesn't support resume, restart from scratch
sleep "$delay"
done
lastError="${lastError//$'\r'/ }"
lastError="${lastError//$'\n'/ }"
appError "Failed to download file after $retries attempts | $lastError"
return 1
}
# Creates an archive from a source file or directory.
#
# Supported archive formats are selected by the target file extension:
# .zip, .tar.gz, or .tgz.
#
# $1 (source): Source file or directory to archive.
# $2 (target): Target archive file path.
#
# Returns:
# 0 on success, 1 on validation or archive creation failure.
function archiveCreate() {
local source="$1"
if [[ -z "$source" ]]; then
appError "Source path not specified"
return 1
fi
if [[ ! -e "$source" ]]; then
appError "Source path not found: $source"
return 1
fi
local target="$2"
if [[ -z "$target" ]]; then
appError "Target file not specified"
return 1
fi
local compressProgram="${3:-}"
local sourcePath sourceBase targetPath targetBase output rc
sourcePath=$(dirname -- "$source")
sourceBase=$(basename -- "$source")
targetPath=$(dirname -- "$target")
targetBase=$(basename -- "$target")
case "$targetBase" in
*.zip|*.tar.gz|*.tgz)
;;
*)
appError "Unknown type archive: $targetBase"
return 1
;;
esac
mkdir -p -- "$targetPath" || {
appError "Failed to create directory: $targetPath"
return 1
}
case "$targetBase" in
*.zip)
if [[ -d "$source" ]]; then
output=$(cd "$source" && zip -qr "$target" . 2>&1)
else
output=$(cd "$sourcePath" && zip -qr "$target" "$sourceBase" 2>&1)
fi
rc=$?
[[ $rc -le 1 ]] || { appError "Error creating ZIP (rc=$rc)${output:+: $output}"; return 1; }
[[ $rc -ne 1 ]] || [[ -z "$output" ]] || appError "Warning creating ZIP${output:+: $output}"
;;
*.tar.gz|*.tgz)
local -a tarCompressFlags
if [[ -n "$compressProgram" ]]; then
tarCompressFlags=("--use-compress-program=$compressProgram")
else
tarCompressFlags=("-z")
fi
if [[ -d "$source" ]]; then
output=$(tar --warning=no-file-changed -c "${tarCompressFlags[@]}" -f "$target" -C "$source" . 2>&1)
else
output=$(tar --warning=no-file-changed -c "${tarCompressFlags[@]}" -f "$target" -C "$sourcePath" "$sourceBase" 2>&1)
fi
rc=$?
[[ $rc -le 1 ]] || { appError "Error creating TAR (rc=$rc)${output:+: $output}"; return 1; }
[[ $rc -ne 1 ]] || [[ -z "$output" ]] || appError "Warning creating TAR${output:+: $output}"
;;
esac
return 0
}
# Extracts an archive into a target directory.
#
# Supported archive formats are selected by the source file extension:
# .zip, .tar.gz, or .tgz.
#
# $1 (source): Source archive file path.
# $2 (target): Target directory where archive contents should be extracted.
#
# Returns:
# 0 on success, 1 on validation or extraction failure.
function archiveExtract() {
local source="$1"
if [[ -z "$source" ]]; then
appError "Source archive not specified"
return 1
fi
if [[ ! -f "$source" ]]; then
appError "Source archive not found: $source"
return 1
fi
local target="$2"
if [[ -z "$target" ]]; then
appError "Target directory not specified"
return 1
fi
local sourceBase output rc
sourceBase=$(basename -- "$source")
case "$sourceBase" in
*.zip|*.tar.gz|*.tgz)
;;
*)
appError "Unknown type archive: $sourceBase"
return 1
;;
esac
mkdir -p -- "$target" || {
appError "Failed to create directory: $target"
return 1
}
case "$sourceBase" in
*.zip)
output=$(unzip -oq "$source" -d "$target" 2>&1)
rc=$?
if [[ $rc -ne 0 ]]; then
appError "Error extracting ZIP: $output"
return 1
fi
;;
*.tar.gz|*.tgz)
output=$(tar -xzf "$source" -C "$target" 2>&1)
rc=$?
if [[ $rc -ne 0 ]]; then
appError "Error extracting TAR: $output"
return 1
fi
;;
esac
return 0
}
# Retrieves the size of the specified path (file or directory) in b/kb/mb/gb.
#
# $1 (path): The path to the file or directory whose size is to be retrieved.
# $2 (unit): Unit.
# $3 (precision): Precision.
function pathSize() {
local path="$1"
local unit="${2:-}"
local precision="${3:-0}"
local divisor="1"
case "${unit,,}" in
kb) divisor="1024" ;;
mb) divisor="1048576" ;;
gb) divisor="1073741824" ;;
esac
[[ -n "$path" ]] || { appError "Path not specified"; return 1; }
local output error bytes
if [[ -d "$path" ]]; then
run output error du -sb "$path" || { appError "$error"; return 1; }
bytes=$(printf '%s\n' "$output" | cut -f1)
elif [[ -f "$path" ]]; then
run output error stat -c %s "$path" || { appError "$error"; return 1; }
bytes="$output"
else
return 1
fi
LC_NUMERIC=C awk -v bytes="$bytes" -v divisor="$divisor" -v precision="$precision" 'BEGIN { printf "%.*f\n", precision, bytes / divisor }'
}
function ftpPathSize() {
local path="$1"
local unit="${2:-}"
local precision="${3:-0}"
local divisor="1"
case "${unit,,}" in
kb) divisor="1024" ;;
mb) divisor="1048576" ;;
gb) divisor="1073741824" ;;
esac
local output error total
if run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$path/" --connect-timeout 10; then
total=0
local line file size
while IFS= read -r line; do
file=$(awk '{print $NF}' <<< "$line")
[[ -n "$file" && "$file" != "." && "$file" != ".." ]] || continue
if [[ "$line" =~ ^d ]]; then
size=$(ftpPathSize "$path/$file") || return 1
else
size=$(awk '{print $5}' <<< "$line")
[[ "$size" =~ ^[0-9]+$ ]] || continue
fi
(( total += size ))
done <<< "$output"
else
local parent name
parent=$(dirname "$path")
name=$(basename "$path")
run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$parent/" --connect-timeout 10 || {
appError "$error"; return 1
}
total=$(awk -v f="$name" '$NF == f {print $5}' <<< "$output")
[[ "$total" =~ ^[0-9]+$ ]] || { appError "Cannot determine size of: $path"; return 1; }
fi
LC_NUMERIC=C awk -v b="$total" -v d="$divisor" -v p="$precision" 'BEGIN { printf "%.*f\n", p, b/d }'
}
# Return the contents of a file if it exists and is non-empty, otherwise write the given value to it and return it.
# $1 (file): path to the file.
# $2 (value): value to write when the file is missing or empty.
function fileValueGetOrCreate() {
local file="${1-}"
local value="${2-}"
if [[ -z "$file" ]]; then
appError "File not specified"
return 1
fi
if [[ -s "$file" ]]; then
cat "$file"
return $?
fi
if [[ -z "$value" ]]; then
appError "Value not specified"
return 1
fi
mkdir -p -- "$(dirname -- "$file")" || {
appError "Failed to create folder for: $file"
return 1
}
install -o root -g root -m 600 /dev/null "$file" || {
appError "Failed to create file: $file"
return 1
}
printf '%s\n' "$value" > "$file" || {
appError "Failed to save value: $file"
return 1
}
printf '%s\n' "$value"
}
# Retrieves or generates a password and stores it in the specified file.
#
# $1: path to the file where the password should be stored.
# [$2+]: additional parameters are passed to the stringRandom function for generating the password (optional).
function filePasswordGetOrCreate() {
local file="${1-}"
if [[ -z "$file" ]]; then
appError "File not specified"
return 1
fi
shift || true
local value
if [[ -s "$file" ]]; then
cat "$file"
return $?
fi
value="$(strRandom "$@")" || {
appError "Failed to generate password"
return 1
}
fileValueGetOrCreate "$file" "$value"
}
# Get value by key from "KEY=VALUE" content or file (returns first match)
function configGet() {
local file="${1-}"
local key="${2-}"
local line value
if [[ -z "$file" ]]; then
appError "Config file not specified"
return 1
fi
if [[ -z "$key" ]]; then
appError "Config key not specified"
return 1
fi
[[ -f "$file" ]] || {
printf '\n'
return 0
}
line="$(awk -F= -v key="$key" '$1 == key { print; exit }' "$file")" || {
printf '\n';
return 0;
}
[[ "$line" == "$key="* ]] || {
printf '\n'
return 0
}
value="${line#*=}"
value="$(sed -E 's/[[:space:]]+$//' <<<"$value")"
printf '%s\n' "$value"
}
# Ensure key=value is present in file (remove previous key lines, then append).
function configSet() {
local file="${1-}"
local key="${2-}"
local value="${3-}"
local tmp
if [[ -z "$file" ]]; then
appError "Config file not specified"
return 1
fi
if [[ -z "$key" ]]; then
appError "Config key not specified"
return 1
fi
value="${value//$'\r'/ }"
value="${value//$'\n'/ }"
mkdir -p -- "$(dirname -- "$file")" || {
appError "Failed to create folder for: $file"
return 1
}
[[ -f "$file" ]] || install -o root -g root -m 600 /dev/null "$file" || {
appError "Failed to create file: $file"
return 1
}
tmp="$(mktemp)" || return 1
awk -F= -v key="$key" '$1 != key' "$file" > "$tmp" || true
if [[ -n "$value" ]]; then
printf '%s=%s\n' "$key" "$value" >> "$tmp" || {
rm -f -- "$tmp"
appError "Failed to write config value: $key"
return 1
}
fi
cat "$tmp" > "$file" || {
rm -f -- "$tmp"
appError "Failed to update config file: $file"
return 1
}
rm -f -- "$tmp"
}
# Get param from config or set via configSet if missing
function configGetOrSet() {
local file="${1-}"
local key="${2-}"
local value="${3-}"
local current
current="$(configGet "$file" "$key")" || return 1
if [[ -n "$current" ]]; then
printf '%s\n' "$current"
return 0
fi
if [[ -z "$value" ]]; then
appError "Config value not specified: $key"
return 1
fi
configSet "$file" "$key" "$value" || return 1
printf '%s\n' "$value"
}
# Get param from config or create via configSet (+gen stringRandom if missing value) if missing
function configGetOrCreate() {
local file="${1-}"
local key="${2-}"
shift 2 || true
local value current
current="$(configGet "$file" "$key")" || return 1
if [[ -n "$current" ]]; then
printf '%s\n' "$current"
return 0
fi
value="$(strRandom "$@")" || {
appError "Failed to generate config value: $key"
return 1
}
configSet "$file" "$key" "$value" || return 1
printf '%s\n' "$value"
}
# Check that no line in a file exceeds the specified maximum length, printing offending lines to stderr.
# $1 (file): path to the file to check.
# $2 (max): maximum allowed line length in characters.
function fileCheckLineLength() {
local file="$1"
local max="$2"
local line len num=0 found=0
[[ -f "$file" ]] || { appError "File not found: $file"; return 1; }
[[ "$max" =~ ^[0-9]+$ ]] || { appError "Invalid max line length: $max"; return 1; }
while IFS= read -r line || [[ -n $line ]]; do
((num++))
len=${#line}
if (( len > max )); then
printf 'Line %d exceeds %d characters (%d)\n' "$num" "$max" "$len" >&2
found=1
fi
done < "$file"
return "$found"
}
function fileSignatureDiffList() {
local path="$1" type="$2" mask="$3"
[[ -n "$path" && -n "$type" && -n "$mask" ]] || { appError "Missing argument(s)"; return 1; }
[[ -d "$path" ]] || { appError "Path not found: $path"; return 1; }
find "$path" -type "$type" -printf '%m:%u:%g:%p\n' 2>/dev/null | grep -vE "$mask:"
return 0
}
function fileSignatureDiff() {
local path="$1" mask="$2" sign
[[ -n "$path" && -n "$mask" ]] || { appError "Missing argument(s)"; return 1; }
[[ -d "$path" ]] || { appError "Path not found: $path"; return 1; }
sign="$(stat -c '%a:%U:%G' "$path")"
[[ "$sign" == "$mask" ]] || printf "%s\n" "$sign:$path";
}
function fileSignatureAclDiff() {
local path="$1" mask="$2"
[[ -n "$path" && -n "$mask" ]] || { printDanger "Missing argument(s)"; return 1; }
[[ -d "$path" ]] || { printDanger "Path not found: $path"; return 1; }
local acl unexpected
acl=$(getfacl -p "$path" 2>/dev/null)
unexpected=$(grep -vE "^(#|\$|(default:)?((user|group|mask)::|other::---|$mask\$))" <<< "$acl" | paste -sd '|')
grep -qxF "$mask" <<< "$acl" || unexpected+="${unexpected:+|}missing:$mask"
grep -qxF "default:$mask" <<< "$acl" || unexpected+="${unexpected:+|}missing:default:$mask"
[[ -z "$unexpected" ]] || printf "%s\n" "$unexpected:$path"
}
function fileSignatureCheck() {
local output error
run output error fileSignatureDiff "$@" || return 1
[[ -z "$output" ]]
}
function fileSignatureAclCheck() {
local output error
run output error fileSignatureAclDiff "$@" || return 1
[[ -z "$output" ]]
}
# Checking the availability of a file (URL) for download.
# Check if a URL is accessible and print the final effective URL on success.
# $1 (url): the URL to check.
function urlAccessible() {
local url="$1" code final
if [[ -z "$url" ]]; then
appError "URL not specified"
return 1
fi
final=$(curl -ksSL --max-redirs 10 --range 0-0 -o /dev/null -w "%{url_effective} %{http_code}" "$url") || return 1
code="${final##* }" # http code
final="${final% *}" # final URL
if [[ "$code" =~ ^[23][0-9]{2}$ ]]; then
printf "%s" "$final"
return 0
fi
return 1
}
# Fetch and print the HTTP status code for the given URL.
# $1 (url): the URL to request.
function urlCode() {
local url="$1" code
if [[ -z "$url" ]]; then
appError "URL not specified"
return 1
fi
code=$(curl -ksSL --max-redirs 10 -o /dev/null -w "%{http_code}" "$url") || return 1
printf "%s" "$code"
return 0
}
# Derive a local filename from a URL, optionally using a custom base name.
# $1 (url): the source URL to extract the filename from.
# [$2] (localFileName): custom base name; if given, the URL extension is appended to it.
function urlLocalFileGen() {
local url="$1" code
if [[ -z "$url" ]]; then
appError "URL not specified"
return 1
fi
local localFileName="$2"
name="${url##*/}"
name="${name%%\?*}"
base="${name%%.*}"
ext="${name#*.}"
if [[ -z "$localFileName" ]]; then
printf '%s' "$name"
else
printf '%s' "$localFileName.$ext"
fi
}
+361
View File
@@ -0,0 +1,361 @@
# Remove a single trailing slash from a string.
# $1 (s): input string.
function strTrimSlash() {
local s="${1-}"
s="${s%/}"
printf '%s' "$s"
}
# Generates a random password with a specified length and character set.
#
# $1 (length): length of the password (defaults to 32 characters).
# $2 (charset): string of characters to use for generating the password (defaults to "A-Za-z0-9@#$%^*_+=[]{}:").
function strRandom() {
local length="${1:-32}"
local charset="${2:-"A-Za-z0-9_.-"}"
LC_ALL=C tr -dc "$charset" < /dev/urandom | head -c "$length"
}
# Format a number with thousands separators (space-separated groups).
# $1 (number): the number to format.
function numFormat() {
printf '%s\n' "${1-}" | sed ':a;s/\B[0-9]\{3\}\>/ &/;ta'
}
# Check if a value exists in an array passed as remaining arguments.
# $1 (needle): value to search for.
# $2 (items): array elements to search in (passed as individual arguments).
function arrayContains() {
local needle="${1-}"
shift || true
local item
for item in "$@"; do
[[ "$item" == "$needle" ]] && return 0
done
return 1
}
# Return success if $value exists as full line in multiline $list
function listContains() {
local value="${1-}"
local list="${2-}"
[[ -n "$value" ]] || return 1
grep -Fxq -- "$value" <<<"$list"
}
# Calculate the difference in seconds between two datetime strings.
# $1 (from): start datetime string (accepted by date -d).
# $2 (to): end datetime string (accepted by date -d).
function timeDiff() {
local from="${1-}"
local to="${2-}"
local t1 t2
[[ -n "$from" ]] || { appError "time_from not specified"; return 1; }
[[ -n "$to" ]] || { appError "time_to not specified"; return 1; }
t1="$(date -d "$from" +%s)" || { appError "invalid time_from: $from"; return 1; }
t2="$(date -d "$to" +%s)" || { appError "invalid time_to: $to"; return 1; }
printf '%s\n' "$(( t2 - t1 ))"
}
# Normalize a domain name: lowercase, strip whitespace and trailing dot, then IDN-encode.
# $1 (domain): raw domain string to normalize.
function domainPrepare() {
local domain="${1-}"
domain="${domain,,}"
domain="${domain//[[:space:]]/}"
domain="${domain%.}"
LC_ALL=C.UTF-8 idn2 <<<"$domain"
}
# Checks if the given string is a valid domain.
#
# $1 (domain): a string representing the domain.
function domainValidate() {
local domain="${1-}"
(( ${#domain} >= 4 )) || { appError "Domain name is too short: $domain"; return 1; }
(( ${#domain} <= 253 )) || { appError "Domain name is too long: $domain"; return 1; }
[[ "$domain" =~ ^([A-Za-z0-9]([-A-Za-z0-9]{0,61}[A-Za-z0-9])?\.)+([A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})$ ]] || { appError "Invalid domain name format: $domain"; return 1; }
}
# Validate a domain name and reject reserved names.
# $1 (domain): domain name to check.
function domainCheck() {
local domain="${1-}"
local reserved=("root" "user")
arrayContains "$domain" "${reserved[@]}" && { appError "Reserved domain name: $domain"; return 1; }
domainValidate "$domain"
}
# Generate username and groupname from domain
function domainToUser() {
local domain="${1-}"
local base hash
base="$(printf '%s' "$domain" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9-]+/-/g; s/-{2,}/-/g; s/^-//; s/-$//')"
hash="$(printf '%s' "$domain$hostSalt" \
| sha256sum \
| awk '{print substr($1,1,8)}')"
printf '%s' "${base:0:21}-${hash}"
}
function domainToUid() {
local domain="${1-}"
local username uid
if [[ -z "$domain" ]]; then
appError "Domain not specified"
return 1
fi
username=$(domainToUser "$domain")
if [[ -z "$username" ]]; then
appError "Cannot compute username for: $domain"
return 1
fi
uid=$(id -u "$username" 2>/dev/null)
if [[ -z "$uid" ]]; then
appError "No such user: $username"
return 1
fi
if [[ "$uid" == "0" ]]; then
appError "Refusing to return root UID for: $domain"
return 1
fi
printf '%s' "$uid"
}
function domainToGid() {
local domain="${1-}"
local username gid
if [[ -z "$domain" ]]; then
appError "Domain not specified"
return 1
fi
username=$(domainToUser "$domain")
if [[ -z "$username" ]]; then
appError "Cannot compute username for: $domain"
return 1
fi
gid=$(id -g "$username" 2>/dev/null)
if [[ -z "$gid" ]]; then
appError "No such user: $username"
return 1
fi
if [[ "$gid" == "0" ]]; then
appError "Refusing to return root GID for: $domain"
return 1
fi
printf '%s' "$gid"
}
# Generate random string from domain
function domainToRandom() {
local domain="${1-}"
local maxLen="${2:-256}"
local tailLen="${3:-8}"
local base tail baseLen
base="$(printf '%s' "$domain" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9]+/_/g; s/_{2,}/_/g; s/^_//; s/_$//')"
tail="$(strRandom "$tailLen" 'a-z0-9')" || return 1
baseLen=$(( maxLen - tailLen - 1 ))
(( baseLen < 1 )) && baseLen=1
printf '%s' "${base:0:baseLen}_${tail}"
}
# Run a command and capture its stdout and stderr into named variables.
# $1 (outVar): name of the variable to receive stdout.
# $2 (errVar): name of the variable to receive stderr.
# $3 (cmd): command and arguments to execute.
function run() {
local -n __runOut="$1"
local -n __runError="$2"
shift 2
local stdoutTmp stderrTmp status
stdoutTmp=$(mktemp) || return 1
stderrTmp=$(mktemp) || { rm -f "$stdoutTmp"; return 1; }
"$@" >"$stdoutTmp" 2>"$stderrTmp"
status=$?
__runOut=$(<"$stdoutTmp")
__runError=$(<"$stderrTmp")
rm -f "$stdoutTmp" "$stderrTmp"
return "$status"
}
# Run command, discard stdout
function runError() {
local -n __runErrorErr="$1"
shift || true
local __runErrorOutput __runErrorError status
run __runErrorOutput __runErrorError "$@"
status=$?
if [[ -n "$__runErrorError" ]]; then
__runErrorErr="$__runErrorError"
else
__runErrorErr="$__runErrorOutput"
fi
return "$status"
}
# Run a command, discarding output, and print an error message on failure.
# $1 (cmd): command and arguments to execute.
function runFail() {
local __runFailErr
runError __runFailErr "$@"
local status=$?
[[ $status -eq 0 ]] || appError "$__runFailErr"
return $status
}
# Run a command and discard all stdout and stderr output.
# $1 (cmd): command and arguments to execute.
function runSilent() {
local output error
run output error "$@"
return $?
}
# Run a shell command string (with operator support) and capture stdout and stderr into named variables.
# $1 (outVar): name of the variable to receive stdout.
# $2 (errVar): name of the variable to receive stderr.
# $3 (cmd): command and arguments, including shell operators (|, &&, ;, etc.).
function runShell() {
local -n __out="$1"
local -n __err="$2"
shift 2
local stdoutTmp stderrTmp status
stdoutTmp=$(mktemp) || return 1
stderrTmp=$(mktemp) || { rm -f "$stdoutTmp"; return 1; }
# Arguments: > >> < | || & && ; ( ) convert to operators.
local cmd="" arg
for arg in "$@"; do
if [[ "$arg" =~ ^([0-9]?>|[0-9]?>>|[0-9]?<|\||\|\||&&|;|\(|\))$ ]]; then
cmd+=" $arg"
else
cmd+=" $(printf '%q' "$arg")"
fi
done
(
set -o pipefail
eval -- "$cmd"
) >"$stdoutTmp" 2>"$stderrTmp"
status=$?
__out=$(<"$stdoutTmp")
__err=$(<"$stderrTmp")
rm -f "$stdoutTmp" "$stderrTmp"
return "$status"
}
# Converts notation (28Gi, 512Mi, 1Ki, 4Gb, ...) to bytes; returns -1 for empty input.
function sizeToBytes() {
local v="$1"
case "$v" in
*Ti) echo $(( ${v%Ti} * 1099511627776 )) ;;
*Gi) echo $(( ${v%Gi} * 1073741824 )) ;;
*Mi) echo $(( ${v%Mi} * 1048576 )) ;;
*Ki) echo $(( ${v%Ki} * 1024 )) ;;
*Tb) echo $(( ${v%Tb} * 1000000000000 )) ;;
*Gb) echo $(( ${v%Gb} * 1000000000 )) ;;
*Mb) echo $(( ${v%Mb} * 1000000 )) ;;
*Kb) echo $(( ${v%Kb} * 1000 )) ;;
"") echo -1 ;;
*) echo "$v" ;;
esac
}
#=========================================================================
# Comment out the first non-empty, non-commented line in the domains list file.
# Prints the domain name on success, returns 1 if no domain available.
function domainsListMark() {
local file="$1"
local line domain num=0 lineNum=0
[[ -f "$file" ]] || { appError "Domains list file not found: $file"; return 1; }
while IFS= read -r line; do
(( num++ ))
[[ -z "$line" || "$line" == \#* ]] && continue
domain="$line"
lineNum=$num
break
done < "$file"
[[ $lineNum -eq 0 ]] && return 1
sed -i "${lineNum}s/^/#/" "$file"
printf '%s\n' "$domain"
}
# Remove the '#' prefix from the specified domain line in the domains list file.
function domainsListUnmark() {
local file="$1"
local domain="$2"
local line num=0 lineNum=0
[[ -f "$file" ]] || { appError "Domains list file not found: $file"; return 1; }
[[ -n "$domain" ]] || { appError "Domain not specified"; return 1; }
while IFS= read -r line; do
(( num++ ))
[[ "$line" == "#${domain}" ]] && { lineNum=$num; break; }
done < "$file"
[[ $lineNum -eq 0 ]] && { appError "Domain not marked in list: $domain"; return 1; }
sed -i "${lineNum}s/^#//" "$file"
}
# Sends an email with the specified subject and body.
#
# $1 (mailSubject): string containing the subject of the email.
# $2 (mailBody): string containing the body of the email.
function emailSend() {
local mailSubject="$1"
local mailBody="$2"
[[ -n "$mailSubject" ]] || { appError "Subject not specified"; return 1; }
[[ -n "$mailBody" ]] || { appError "Body email not specified"; return 1; }
local result
result=$(printf 'Subject:%s\nFrom:%s\nTo:%s\n\n%s' "$mailSubject" "$mailFrom" "$mailTo" "$mailBody" | msmtp "$mailTo" 2>&1)
[[ $? -eq 0 ]] || { appError "$result"; return 1; }
}

Some files were not shown because too many files have changed in this diff Show More