jina verze
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
.zed/
|
||||
_tmp/
|
||||
data/
|
||||
config.sh
|
||||
_gen.sh
|
||||
CLAUDE.md
|
||||
codebook.toml
|
||||
@@ -0,0 +1,123 @@
|
||||
# KUBE 7.0.537
|
||||
|
||||
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
|
||||
``````
|
||||
@@ -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
|
||||
@@ -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 }}-config-pv" }
|
||||
spec:
|
||||
capacity: { storage: 1Gi }
|
||||
volumeMode: Filesystem
|
||||
accessModes: [ ReadWriteMany ]
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
hostPath: { path: "{{ .Values.openlitespeedConfigPath }}", 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.openlitespeedAdminPath }}", 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.openlitespeedPhpIniPath }}", 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.openlitespeedLogsPath }}", 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: v1
|
||||
kind: PersistentVolume
|
||||
metadata: { name: "{{ .Values.openlitespeed }}-vhost-data-pv" }
|
||||
spec:
|
||||
capacity: { storage: 5Gi }
|
||||
volumeMode: Filesystem
|
||||
accessModes: [ ReadWriteMany ]
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
hostPath: { path: "{{ .Values.openlitespeedVhostDataPath }}", type: DirectoryOrCreate }
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata: { name: "{{ .Values.openlitespeed }}-vhost-data-pvc", namespace: "{{ .Values.namespace }}" }
|
||||
spec:
|
||||
volumeName: "{{ .Values.openlitespeed }}-vhost-data-pv"
|
||||
volumeMode: Filesystem
|
||||
accessModes: [ ReadWriteMany ]
|
||||
resources: { requests: { storage: 5Gi } }
|
||||
storageClassName: ""
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata: { name: "{{ .Values.openlitespeed }}-vhost-shared-pv" }
|
||||
spec:
|
||||
capacity: { storage: 1Gi }
|
||||
volumeMode: Filesystem
|
||||
accessModes: [ ReadWriteMany ]
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
hostPath: { path: "{{ .Values.openlitespeedVhostSharedPath }}", type: DirectoryOrCreate }
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata: { name: "{{ .Values.openlitespeed }}-vhost-shared-pvc", namespace: "{{ .Values.namespace }}" }
|
||||
spec:
|
||||
volumeName: "{{ .Values.openlitespeed }}-vhost-shared-pv"
|
||||
volumeMode: Filesystem
|
||||
accessModes: [ ReadWriteMany ]
|
||||
resources: { requests: { storage: 1Gi } }
|
||||
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: /var/www/vhosts }
|
||||
- { 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 }
|
||||
- { name: "{{ .Values.openlitespeed }}-vhost-data-volume", mountPath: /var/www/data, readOnly: true }
|
||||
- { name: "{{ .Values.openlitespeed }}-vhost-shared-volume", mountPath: /var/www/shared, readOnly: true }
|
||||
volumes:
|
||||
- name: "{{ .Values.openlitespeed }}-vhosts-volume"
|
||||
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-vhosts-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 }
|
||||
- name: "{{ .Values.openlitespeed }}-vhost-data-volume"
|
||||
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-vhost-data-pvc" }
|
||||
- name: "{{ .Values.openlitespeed }}-vhost-shared-volume"
|
||||
persistentVolumeClaim: { claimName: "{{ .Values.openlitespeed }}-vhost-shared-pvc" }
|
||||
|
||||
---
|
||||
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.openlitespeedAdminWhiteList }} ] }
|
||||
|
||||
---
|
||||
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 }}
|
||||
@@ -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'
|
||||
@@ -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,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,33 @@
|
||||
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/https.rules
|
||||
rewriteFile /usr/local/lsws/conf/rules/front-controller.rules
|
||||
END_rules
|
||||
}
|
||||
@@ -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,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();
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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.")
|
||||
@@ -0,0 +1,155 @@
|
||||
# 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"
|
||||
vhostsPath="$basePath/vhosts"
|
||||
domainsList="$appAssetsPath/domains.list"
|
||||
sftpAccessGroup="sftp-access"
|
||||
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
|
||||
openlitespeedKube="openlitespeed"
|
||||
openlitespeedYaml="$appDataPath/yaml/$openlitespeedKube.yaml"
|
||||
openlitespeedPath="$basePath/$openlitespeedKube"
|
||||
openlitespeedAdminPath="$openlitespeedPath/admin"
|
||||
openlitespeedPhpIniPath="$openlitespeedPath/php-ini"
|
||||
openlitespeedConfigPath="$openlitespeedPath/config"
|
||||
openlitespeedLogsPath="$openlitespeedPath/logs"
|
||||
openlitespeedVhostDataPath="$openlitespeedPath/vhost-data"
|
||||
openlitespeedVhostSharedPath="$openlitespeedPath/vhost-shared"
|
||||
openlitespeedVhostsPath="$openlitespeedConfigPath/vhosts"
|
||||
openlitespeedConfigFile="$openlitespeedConfigPath/httpd_config.conf"
|
||||
openlitespeedAdminWhiteList=("0.0.0.0/0")
|
||||
openlitespeedAdminPass=$(filePasswordGetOrCreate "$appDataPath/$hostName.$openlitespeedKube")
|
||||
openlitespeedQuotaBlockLimit=10485760
|
||||
openlitespeedQuotaInodeLimit=100000
|
||||
|
||||
# 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"
|
||||
postfixResolver="@8.8.8.8"
|
||||
|
||||
# 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
|
||||
storagePath="/$hostName"
|
||||
storageType="rsync"
|
||||
storageDays=99
|
||||
|
||||
# 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"
|
||||
)
|
||||
@@ -0,0 +1,151 @@
|
||||
[KUBE](../README.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
[KUBE](../README.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
[KUBE](../README.md) / 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
|
||||
@@ -0,0 +1,14 @@
|
||||
[KUBE](../README.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
[KUBE](../README.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
[KUBE](../README.md) / 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]:
|
||||
```
|
||||
@@ -0,0 +1,286 @@
|
||||
[KUBE](../README.md) / 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`
|
||||
@@ -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
|
||||
```
|
||||
---
|
||||
@@ -0,0 +1,129 @@
|
||||
[KUBE](../README.md) / 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
|
||||
- `vhostsPath` - 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
|
||||
***
|
||||
|
||||
- `openlitespeedKube` - Openlitespeed identifier in k3s
|
||||
- `openlitespeedYaml` - Path to file template YAML for OpenLiteSpeed
|
||||
- `openlitespeedPath` - Directory for OpenLiteSpeed configuration
|
||||
- `openlitespeedAdminPath` - Directory for OpenLiteSpeed Admin configuration
|
||||
- `openlitespeedConfigFile` - OpenLiteSpeed configuration file
|
||||
- `openlitespeedVhostsPath` - Directory for vhosts configuration files
|
||||
- `openlitespeedAdminPass` - 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 (%)
|
||||
***
|
||||
@@ -0,0 +1,167 @@
|
||||
[KUBE](../../README.md) / [k3s](../k3s.md) / 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) / [k3s](../k3s.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
[KUBE](../../README.md) / [k3s](../k3s.md) / 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; ...
|
||||
```
|
||||
@@ -0,0 +1,163 @@
|
||||
[KUBE](../../README.md) / [k3s](../k3s.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
[KUBE](../../README.md) / [k3s](../k3s.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
[KUBE](../README.md) / 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.
|
||||
@@ -0,0 +1,24 @@
|
||||
[KUBE](../README.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
[KUBE](../README.md) / 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]:
|
||||
```
|
||||
@@ -0,0 +1,191 @@
|
||||
[KUBE](../README.md) / 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 (`vhostsPath`)
|
||||
* 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 (`vhostsPath`)
|
||||
* 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 `$vhostsPath/<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 `$vhostsPath/<domain>/www/wp-config.php`
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
[KUBE](../README.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
[KUBE](../README.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,214 @@
|
||||
[KUBE](../README.md) / 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
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
readonly appVersion="7.0.537"
|
||||
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 $?
|
||||
@@ -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
|
||||
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' 'openlitespeedKube' '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' 'vhostsPath' 'domainsList' 'k3sCmd' 'redisPath' 'mariadbMasterPath' 'mariadbSlavePath' 'openlitespeedPath' 'openlitespeedAdminPath' 'openlitespeedPhpIniPath' 'openlitespeedLogsPath' 'openlitespeedConfigFile' 'openlitespeedVhostDataPath' 'openlitespeedVhostSharedPath' 'openlitespeedVhostsPath' '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' 'openlitespeedQuotaBlockLimit' 'openlitespeedQuotaInodeLimit')
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
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 openlitespeedVhostList || { printDanger "Failed get list of virtual hosts: $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 openlitespeedVhostList || { printDanger "Failed get list of virtual hosts: $error"; return 1; }
|
||||
|
||||
printText "Files..."
|
||||
if [[ "$path" == *"$backupFirstDir" ]]; then
|
||||
runError error rsync -a --mkpath -- "$vhostsPath/" "$path" || printDanger "$error"
|
||||
else
|
||||
runError error rsync -a --link-dest="$backupDailyPath/$backupFirstDir" -- "$vhostsPath/" "$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 -- "$openlitespeedVhostsPath/$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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
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"
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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"
|
||||
|
||||
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
|
||||
printRow
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
function cmdHelpPrint() {
|
||||
printf "%b" "\n$fontYellow $1$fontReset\n$2\n\n"
|
||||
}
|
||||
|
||||
function cmdHelpK3S() {
|
||||
local title="-k|--k3s <action> [option]"
|
||||
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
|
||||
restart_php - restart PHP
|
||||
kill_php - kill PHP (hard restart)
|
||||
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 $openlitespeedConfigFile"
|
||||
|
||||
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
|
||||
list <option> - list (domain|alias|senders|sasl) !!!!
|
||||
domain <domain> - add domain
|
||||
alias <domain> <email> - set alias for domain
|
||||
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
|
||||
task <file> [domain] - Execute task !!!!!
|
||||
list - task list
|
||||
down - stop receiving new tasks from the API (pause)
|
||||
up - start receiving new tasks from the API (unpause)"
|
||||
|
||||
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
|
||||
exec <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="-b|--backup <target> [path] [type]"
|
||||
local path=$(backupPathGenerate)
|
||||
local desc="
|
||||
target - all|<domain>
|
||||
path - path to save backup, default value: $path (autogenerate)
|
||||
type - type backup (zip|tar|archive|rsync), default value: $backupType"
|
||||
cmdHelpPrint "$title" "$desc"
|
||||
}
|
||||
|
||||
function cmdHelpRestore() {
|
||||
local title="--restore <domain> [option] [NO TESTED!]"
|
||||
local desc="
|
||||
<domain> - manual site restore
|
||||
<domain> list - display a list of available markers for restore
|
||||
<domain> last - automatic site restore from the last backup
|
||||
<domain> full - automatic site restore from the last FULL backup
|
||||
<domain> auto - automatic site restore from the last FULL backup or the last backup
|
||||
<domain> 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
|
||||
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> [NO TESTED!]"
|
||||
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="
|
||||
muplugin - check MU 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 -b|--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
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
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() {
|
||||
local cli="$1"
|
||||
local pod
|
||||
cmdK3sPodSelect pod || { printRow; 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[@]}"
|
||||
printRow
|
||||
}
|
||||
|
||||
# Interactively selects a pod and streams its logs.
|
||||
# [$@] (...): extra arguments passed to kubectl logs (e.g. -f, --tail=100).
|
||||
function cmdLog() {
|
||||
local pod
|
||||
cmdK3sPodSelect pod || { printRow; 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[@]}" "$@"
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
case "$target" in
|
||||
pod)
|
||||
local pod
|
||||
cmdK3sPodSelect pod || { printRow; return 1; }
|
||||
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace describe pod/$pod $*$fontReset"
|
||||
printRow
|
||||
k3sRun describe "pod/$pod" "$@"
|
||||
k3sRun describe "pod/$pod" "$@"
|
||||
printRow
|
||||
;;
|
||||
node)
|
||||
k3sRun describe node "$@"
|
||||
printRow
|
||||
;;
|
||||
*)
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $target"
|
||||
cmdHelpDescribe
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Interactively selects and deletes a pod.
|
||||
function cmdDelete() {
|
||||
local pod
|
||||
cmdK3sPodSelect pod || { printRow; return 1; }
|
||||
|
||||
printText "$fontBlue$k3sCmd kubectl -n $k3sNamespace delete pod/$pod $*$fontReset"
|
||||
printRow
|
||||
k3sRun delete "pod/$pod" "$@"
|
||||
printRow
|
||||
}
|
||||
|
||||
# Lists k3s resources; interactively prompts for type if not provided.
|
||||
# [$1] (resource): resource type abbreviation (pod, deploy, svc, etc.).
|
||||
function cmdK3sResourceList() {
|
||||
printRow
|
||||
|
||||
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"
|
||||
|
||||
local i=0 line code info num
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
((i++))
|
||||
num=$(printf "%3d" "$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
|
||||
[[ -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%%|*}"
|
||||
|
||||
printTitle " $k3sLabel Resources $resource"
|
||||
printRow
|
||||
fi
|
||||
|
||||
run output error k3sRun get "$resource"|| { printDanger "$error"; return 1; }
|
||||
printf '%s\n' "$output"
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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"
|
||||
|
||||
printRow
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
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"
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
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"
|
||||
printRow
|
||||
}
|
||||
|
||||
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 "$vhostsPath" d || { printDanger "$error"; return 1; }
|
||||
while read -r dir; do
|
||||
local found=0
|
||||
run itemList error fileList "$vhostsPath/$dir/www/wp-content/mu-plugins/" f || continue
|
||||
while read -r item; do
|
||||
if grep -qF '($i){static $a=null' "$vhostsPath/$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 "$item" "${fontYellow}warning$fontReset"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$itemList")
|
||||
done < <(awk 'NF' <<< "$vhostsList")
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
function cmdMalwareFilesCheck() {
|
||||
local target="$1"
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedVhostList; 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
printRow
|
||||
}
|
||||
|
||||
function cmdMalwareOptionsSuspiciousNames2() {
|
||||
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 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 -k3,3
|
||||
printRow
|
||||
}
|
||||
|
||||
function cmdMalwareOptionsSuspiciousNames3() {
|
||||
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",
|
||||
" 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 -k3,3
|
||||
printRow
|
||||
}
|
||||
|
||||
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
|
||||
printRow
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
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 "Database"
|
||||
|
||||
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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
@@ -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,528 @@
|
||||
openlitespeedLabel="[OpenLiteSpeed]"
|
||||
|
||||
# Renders the OpenLiteSpeed Kubernetes YAML manifest via Helm.
|
||||
function cmdOpenlitespeedYamlRender() {
|
||||
local templateFile="templates/$openlitespeedKube.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 "${!openlitespeedAdminWhiteList[@]}"; do
|
||||
adminWhiteList[$i]="\"${openlitespeedAdminWhiteList[$i]}\""
|
||||
done
|
||||
adminWhiteStr=$(IFS=','; printf '%s\n' "${adminWhiteList[*]}")
|
||||
|
||||
local varsFile
|
||||
varsFile=$(mktemp "/tmp/$openlitespeedKube.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: $openlitespeedKube
|
||||
openlitespeedConfigPath: $openlitespeedConfigPath
|
||||
openlitespeedPhpIniPath: $openlitespeedPhpIniPath
|
||||
openlitespeedLogsPath: $openlitespeedLogsPath
|
||||
openlitespeedVhostDataPath: $openlitespeedVhostDataPath
|
||||
openlitespeedVhostSharedPath: $openlitespeedVhostSharedPath
|
||||
openlitespeedAdminPath: $openlitespeedAdminPath
|
||||
openlitespeedAdminWhiteList: $adminWhiteStr
|
||||
vhostsPath: $vhostsPath
|
||||
EOF
|
||||
|
||||
printDotText "Result" "$openlitespeedYaml"
|
||||
mkdir -p -- "$(dirname -- "$openlitespeedYaml")" || return 1
|
||||
fileBackup "$openlitespeedYaml"
|
||||
helm template stack "$appAssetsPath/k3s" -f "$varsFile" -f "$profileCpuFile" -f "$profileMemoryFile" --show-only "$templateFile" > "$openlitespeedYaml" || {
|
||||
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() {
|
||||
printInfo "$openlitespeedLabel Initialization..."
|
||||
|
||||
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")
|
||||
|
||||
"$k3sCmd" kubectl -n kube-system patch svc traefik -p '{"spec": {"externalTrafficPolicy": "Local"}}'
|
||||
cmdOpenlitespeedWaitReady || return 1
|
||||
cmdOpenlitespeedAdminPassUpdate "$openlitespeedAdminPass" || return 1
|
||||
|
||||
local ug output error
|
||||
run ug error stat -c "%u:%g" "$openlitespeedConfigFile" || printDanger "$openlitespeedLabel Stat: $error"
|
||||
|
||||
printInfo "$openlitespeedLabel Adding PHP configs..."
|
||||
local phpIniFile="$openlitespeedPhpIniPath/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 "$openlitespeedPhpIniPath" -type f -exec chmod 644 {} +
|
||||
|
||||
printInfo "$openlitespeedLabel Adding redirect rules..."
|
||||
mkdir -p "$openlitespeedConfigPath/rules"
|
||||
cp -n "$appAssetsPath"/openlitespeed/rules/* "$openlitespeedConfigPath/rules/"
|
||||
chown -R "$ug" "$openlitespeedConfigPath/rules"
|
||||
chmod 750 -R "$openlitespeedConfigPath/rules"
|
||||
|
||||
printInfo "$openlitespeedLabel Path OLS config..."
|
||||
fileBackup "$openlitespeedConfigFile"
|
||||
openlitespeedConfigEditSet '' useIpInProxyHeader 2 || return 1
|
||||
openlitespeedConfigEditSet 'fileAccessControl' checkSymbolLink 1 || return 1
|
||||
openlitespeedConfigEditSet 'accessControl' allow '10.42.0.0/16T, 10.43.0.0/16T' || return 1
|
||||
openlitespeedConfigEditDel 'ext[Pp]rocessor\h+lsphp' env 'PHPRC=*' || return 1
|
||||
openlitespeedConfigEditSet 'ext[Pp]rocessor\h+lsphp' backlog 100 || return 1
|
||||
openlitespeedConfigEditSet 'ext[Pp]rocessor\h+lsphp' procSoftLimit 700 || return 1
|
||||
openlitespeedConfigEditSet 'ext[Pp]rocessor\h+lsphp' procHardLimit 800 || return 1
|
||||
openlitespeedConfigEditSet 'ext[Pp]rocessor\h+lsphp' maxConns "$children" || return 1
|
||||
openlitespeedConfigEditSetMasked 'ext[Pp]rocessor\h+lsphp' env 'PHP_INI_SCAN_DIR=*' 'PHP_INI_SCAN_DIR=:/etc/ols-php-ini' || return 1
|
||||
openlitespeedConfigEditSetMasked 'ext[Pp]rocessor\h+lsphp' env 'PHP_LSAPI_CHILDREN=*' "PHP_LSAPI_CHILDREN=$children" || return 1
|
||||
openlitespeedConfigEditSetMasked 'ext[Pp]rocessor\h+lsphp' env 'LSAPI_AVOID_FORK=*' 'LSAPI_AVOID_FORK=0' || return 1
|
||||
openlitespeedConfigEditSetMasked 'ext[Pp]rocessor\h+lsphp' env 'LSAPI_MAX_IDLE=*' 'LSAPI_MAX_IDLE=120' || return 1
|
||||
openlitespeedConfigEditSetMasked 'ext[Pp]rocessor\h+lsphp' env 'LSAPI_MAX_IDLE_CHILDREN=*' 'LSAPI_MAX_IDLE_CHILDREN=1' || return 1
|
||||
openlitespeedConfigEditSetMasked 'ext[Pp]rocessor\h+lsphp' env 'LSAPI_PGRP_MAX_IDLE=*' 'LSAPI_PGRP_MAX_IDLE=300' || return 1
|
||||
openlitespeedConfigEditSetMasked 'ext[Pp]rocessor\h+lsphp' env 'LSAPI_MAX_PROCESS_TIME=*' 'LSAPI_MAX_PROCESS_TIME=300' || return 1
|
||||
openlitespeedConfigEditSetMasked 'ext[Pp]rocessor\h+lsphp' env 'LSAPI_SLOW_REQ_MSECS=*' 'LSAPI_SLOW_REQ_MSECS=5000' || return 1
|
||||
|
||||
printInfo "$openlitespeedLabel Path OLS Admin config..."
|
||||
openlitespeedAdminAllowList || return 1
|
||||
|
||||
cmdOpenlitespeedRestart
|
||||
cmdOpenlitespeedPhpRestart
|
||||
|
||||
printSuccess "$openlitespeedLabel Initialization completed"
|
||||
}
|
||||
|
||||
# Updates OpenLiteSpeed Kubernetes resources (limits, replicas, etc.) without re-initialization.
|
||||
function cmdOpenlitespeedUpdate() {
|
||||
cmdOpenlitespeedYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$openlitespeedYaml" || return 1
|
||||
}
|
||||
|
||||
# Installs OpenLiteSpeed into Kubernetes and runs initialization.
|
||||
function cmdOpenlitespeedInstall() {
|
||||
cmdOpenlitespeedYamlRender || return 1
|
||||
cmdK3sYamlApplyEx "$openlitespeedYaml" || return 1
|
||||
cmdOpenlitespeedInit
|
||||
}
|
||||
|
||||
# Uninstalls OpenLiteSpeed Kubernetes resources.
|
||||
function cmdOpenlitespeedUninstall() {
|
||||
"$k3sCmd" kubectl delete -f "$openlitespeedYaml"
|
||||
}
|
||||
|
||||
# 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" || {
|
||||
printDanger "$openlitespeedLabel Create pass | $error"
|
||||
return 1
|
||||
}
|
||||
|
||||
run output error openlitespeedExec bash -c "echo '$user:$encrypt' > /usr/local/lsws/admin/conf/htpasswd" || {
|
||||
printDanger "$openlitespeedLabel Encrypt pass | $error"
|
||||
return 1
|
||||
}
|
||||
|
||||
printSuccess "$openlitespeedLabel Authorization parameters updated"
|
||||
}
|
||||
|
||||
# Restarts OpenLiteSpeed on all OLS pods.
|
||||
function cmdOpenlitespeedRestart() {
|
||||
local output error podList
|
||||
|
||||
if ! run podList error k3sPodListStatus "$openlitespeedKube"; 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 restart; then
|
||||
printDotText "Restart" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
elif [[ "$output" =~ "[OK]" ]]; then
|
||||
printDotText "Restart" "$fontGreen$output$fontReset"
|
||||
else
|
||||
printDotText "Restart" "$fontRed$output$fontReset"
|
||||
fi
|
||||
|
||||
# k3sRun wait --for=condition=Ready "pod/$pod" --timeout=10s >/dev/null 2>&1 || true
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Restarts PHP workers on all OLS pods.
|
||||
function cmdOpenlitespeedPhpRestart() {
|
||||
local output error podList
|
||||
|
||||
if ! run podList error k3sPodListStatus "$openlitespeedKube"; 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"
|
||||
run output error openlitespeedPodExec "$pod" killall -USR1 lsphp || true
|
||||
printDotText "PHP" "process restart"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Restarts PHP (kill) workers on all OLS pods.
|
||||
function cmdOpenlitespeedPhpKill() {
|
||||
local output error podList
|
||||
|
||||
if ! run podList error k3sPodListStatus "$openlitespeedKube"; 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"
|
||||
run output error openlitespeedPodExec "$pod" pkill -9 -f lsphp || true
|
||||
printDotText "PHP" "process kill"
|
||||
fi
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
fi
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Prints OpenLiteSpeed and PHP versions for each OLS pod.
|
||||
function cmdOpenlitespeedInfo() {
|
||||
local output error podList ver pid
|
||||
|
||||
if ! run podList error k3sPodListStatus "$openlitespeedKube"; 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 openlitespeedVhostList; then
|
||||
mapfile -t vhostList < <(awk 'NF' <<< "$output")
|
||||
printDotText "all" "${#vhostList[@]}"
|
||||
else
|
||||
printDotText "all" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
if run output error openlitespeedVhostList "down"; then
|
||||
mapfile -t vhostDownList < <(awk 'NF' <<< "$output")
|
||||
printDotText "down" "${#vhostDownList[@]}"
|
||||
else
|
||||
printDotText "down" "$labelUnknown"
|
||||
printDanger "$error"
|
||||
fi
|
||||
|
||||
local count="$(find "$vhostsPath" -mindepth 1 -maxdepth 1 -type d | wc -l)"
|
||||
printDotText "directories" "$fontGray$vhostsPath$fontReset $count"
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Marks a virtual host as suspended.
|
||||
# $1 (domain): site domain name.
|
||||
function cmdOpenlitespeedVHostDown() {
|
||||
local error
|
||||
|
||||
printRow
|
||||
|
||||
if ! runError error openlitespeedVHostDown "$@"; then
|
||||
printDotText "VHost down" "$labelFail"
|
||||
printDanger "$error"
|
||||
printRow
|
||||
else
|
||||
printDotText "VHost down" "$labelPass"
|
||||
cmdOpenlitespeedRestart
|
||||
fi
|
||||
}
|
||||
|
||||
# Marks a virtual host as active.
|
||||
# $1 (domain): site domain name.
|
||||
function cmdOpenlitespeedVHostUp() {
|
||||
local error
|
||||
|
||||
printRow
|
||||
|
||||
if ! runError error openlitespeedVHostUp "$@"; then
|
||||
printDotText "VHost up" "$labelFail"
|
||||
printDanger "$error"
|
||||
printRow
|
||||
else
|
||||
printDotText "VHost up" "$labelPass"
|
||||
cmdOpenlitespeedRestart
|
||||
fi
|
||||
}
|
||||
|
||||
# Lists virtual hosts with optional status filtering.
|
||||
# [$1] (type): all (default) | up | down.
|
||||
function cmdOpenlitespeedSiteList() {
|
||||
local output error type="${1:-all}"
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error openlitespeedVhostList "$type"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Updates the Traefik middleware allowlist for OpenLiteSpeed WebAdmin.
|
||||
function cmdOpenlitespeedAdminWhiteList() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error openlitespeedAdminWhiteList; then
|
||||
printDotText "Update" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "White list" "$output"
|
||||
printDotText "Update" "$labelDone"
|
||||
fi
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Sets aliases for an OpenLiteSpeed virtual host.
|
||||
# $1 (domain): primary site domain name.
|
||||
# $@ (...): alias domain names.
|
||||
function cmdOpenlitespeedAliasSet() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error openlitespeedAliasSet "$@"; then
|
||||
printDanger "$error"
|
||||
printRow
|
||||
elif [[ -z "$output" ]]; then
|
||||
printText "$labelNull"
|
||||
cmdOpenlitespeedRestart
|
||||
else
|
||||
printText "$output"
|
||||
cmdOpenlitespeedRestart
|
||||
fi
|
||||
}
|
||||
|
||||
# Updates OpenLiteSpeed WebAdmin access control from current Traefik pod IPs.
|
||||
function cmdOpenlitespeedAdminAllowList() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
if ! run output error openlitespeedAdminAllowList; then
|
||||
printDotText "Update" "$labelFail"
|
||||
printDanger "$error"
|
||||
printRow
|
||||
elif [[ -z "$output" ]]; then
|
||||
printDotText "Allow list" "$labelNull"
|
||||
printDotText "Update" "$labelDone"
|
||||
cmdOpenlitespeedRestart
|
||||
else
|
||||
printDotText "Allow list" "$output"
|
||||
printDotText "Update" "$labelDone"
|
||||
cmdOpenlitespeedRestart
|
||||
fi
|
||||
}
|
||||
|
||||
# Lists aliases for a domain or all domains.
|
||||
# [$1] (target): domain name, or "all" to list all.
|
||||
function cmdOpenlitespeedAliasList() {
|
||||
local output error domain target="$1"
|
||||
|
||||
printRow
|
||||
|
||||
if [[ "$target" == all ]]; then
|
||||
if ! run output error openlitespeedAliasList; then
|
||||
printDanger "$error"
|
||||
elif [[ -z "$output" ]]; then
|
||||
printText "$labelNull"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
else
|
||||
domain=$(domainPrepare "$target")
|
||||
if ! run output error openlitespeedVhostAlias "$domain"; then
|
||||
printDanger "$error"
|
||||
elif [[ -z "$output" ]]; then
|
||||
printText "$labelNull"
|
||||
else
|
||||
printText "$output"
|
||||
fi
|
||||
fi
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Tests the OpenLiteSpeed configuration inside the container.
|
||||
function cmdOpenlitespeedConfigCheck() {
|
||||
local output error
|
||||
|
||||
printRow
|
||||
|
||||
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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
function cmdOpenlitespeedVhostRebuild() {
|
||||
local target="$1"
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedVhostList; 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
|
||||
|
||||
printRow
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
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() {
|
||||
postfixDkimAdd "$postfixHost" || return 1
|
||||
local error
|
||||
if ! runError error postfixExec chown postfix:postfix /config/sasldb2; then
|
||||
printDanger "$postfixLabel | $error"
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Uninstalls Postfix Kubernetes resources.
|
||||
function cmdPostfixUninstall() {
|
||||
"$k3sCmd" kubectl delete -f "$postfixYaml"
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Sets a virtual alias forward-to address for a domain and saves it to site config.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (mail): forward-to email address.
|
||||
function cmdPostfixVirtualAliasSetEx() {
|
||||
local domain="$1"
|
||||
[[ -n "$domain" ]] || { printDanger "$postfixLabel Domain not specified"; return 1; }
|
||||
local mail="$2"
|
||||
[[ -n "$mail" ]] || { printDanger "$postfixLabel Mail not specified"; return 1; }
|
||||
|
||||
local domainList output error
|
||||
if ! run domainList error postfixDomainList; then
|
||||
printDanger "$postfixLabel postfixDomainList: $error"
|
||||
return 1
|
||||
elif ! listContains "$domain" "$domainList"; then
|
||||
printDanger "$postfixLabel Domain not found in postfixDomainList"
|
||||
return 1
|
||||
elif ! run output error siteConfigSet "$domain" "postfixForwardTo" "$mail"; then
|
||||
printDanger "$postfixLabel siteConfigSet: $error"
|
||||
elif ! run output error postfixVirtualAliasSet "@$domain" "$mail"; then
|
||||
printDanger "$postfixLabel postfixVirtualAliasSet: $error"
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks MTA host DNS records (A, SPF, PTR, DKIM) against configured values.
|
||||
function cmdPostfixMtaDnsCheck() {
|
||||
local label output error text
|
||||
|
||||
label="$postfixLabel A record: $postfixHost"
|
||||
if ! run output error dig +short A "$postfixHost" "$postfixResolver"; then
|
||||
printDanger "$label"
|
||||
else
|
||||
text=$(printf '%s' "$output" | paste -sd, - | sed 's/,/ \/ /g')
|
||||
if grep -Fxq -- "$postfixIp" <<<"$output"; then
|
||||
printSuccess "$label | $text"
|
||||
else
|
||||
printWarning "$label | $text"
|
||||
fi
|
||||
fi
|
||||
|
||||
label="$postfixLabel SPF record: $postfixHost"
|
||||
if ! run output error dig +short TXT "$postfixHost" "$postfixResolver"; then
|
||||
printDanger "$label"
|
||||
elif grep -Eq '^"?v=spf1([[:space:]]|")' <<<"$output"; then
|
||||
printSuccess "$label | $output"
|
||||
else
|
||||
printWarning "$label | $output"
|
||||
fi
|
||||
|
||||
label="$postfixLabel PTR record: $postfixHost"
|
||||
if ! run output error dig -x "$postfixIp" +short "$postfixResolver"; then
|
||||
printDanger "$label"
|
||||
else
|
||||
text=$(printf '%s' "$output" | paste -sd, - | sed 's/,/ \/ /g')
|
||||
if grep -Fxq -- "$postfixHost." <<<"$output"; then
|
||||
printSuccess "$label | $text"
|
||||
else
|
||||
printWarning "$label | $text"
|
||||
fi
|
||||
fi
|
||||
|
||||
label="$postfixLabel DKIM record: $postfixHost"
|
||||
if ! run output error dig +short TXT "$postfixDkimSelector._domainkey.$postfixHost"; then
|
||||
printDanger "$label | $error"
|
||||
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
|
||||
printSuccess "$label | OK"
|
||||
else
|
||||
printWarning "$label | DNS : $dkimDnsNorm"
|
||||
printWarning "$label | FILE: $dkimFileNorm"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Prints domain/alias/sender/SASL lists; accepts domain/alias/senders/sasl as filter.
|
||||
# [$1] (filter): domain|alias|senders|sasl; omit to print all.
|
||||
function cmdPostfixList() {
|
||||
case "$1" in
|
||||
'domain')
|
||||
postfixDomainList
|
||||
;;
|
||||
'alias')
|
||||
postfixAliasList
|
||||
;;
|
||||
'senders')
|
||||
postfixSendersList
|
||||
;;
|
||||
'sasl')
|
||||
postfixSaslUserList
|
||||
;;
|
||||
*)
|
||||
printInfo "$postfixLabel Domains"
|
||||
postfixDomainList
|
||||
|
||||
printInfo "$postfixLabel Aliases"
|
||||
postfixAliasList
|
||||
|
||||
printInfo "$postfixLabel Senders"
|
||||
postfixSendersList
|
||||
|
||||
printInfo "$postfixLabel SASL users"
|
||||
postfixSaslUserList
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
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 "$openlitespeedKube"; 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
@@ -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="$vhostsPath/$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" "$openlitespeedVhostsPath/$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
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
# 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"
|
||||
cmdOpenlitespeedAliasSet "$@"
|
||||
;;
|
||||
restart)
|
||||
printTitle " $openlitespeedLabel Restart"
|
||||
cmdOpenlitespeedRestart
|
||||
;;
|
||||
restart_php)
|
||||
printTitle " $openlitespeedLabel Restart PHP"
|
||||
cmdOpenlitespeedPhpRestart
|
||||
;;
|
||||
kill_php)
|
||||
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
|
||||
;;
|
||||
list)
|
||||
if [[ -z "$1" ]]; then
|
||||
printInfo " $postfixLabel List all"
|
||||
else
|
||||
printInfo " $postfixLabel List $action"
|
||||
fi
|
||||
cmdPostfixList "$@"
|
||||
;;
|
||||
domain)
|
||||
printInfo " $postfixLabel Domain add"
|
||||
postfixDomainAdd "$@"
|
||||
postfixPostmapRebuild
|
||||
;;
|
||||
alias)
|
||||
printInfo " $postfixLabel Set alias for domain"
|
||||
cmdPostfixVirtualAliasSetEx "$@"
|
||||
;;
|
||||
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() {
|
||||
if [[ -n "$1" ]]; then
|
||||
cmdBackupRun "$@"
|
||||
else
|
||||
printDanger "$backupLabel Empty command"
|
||||
cmdHelpBackup
|
||||
fi
|
||||
}
|
||||
|
||||
function cmdRestore() {
|
||||
if [[ -n "$1" ]]; then
|
||||
restoreRun "$@"
|
||||
else
|
||||
printDanger "$restoreLabel Empty command"
|
||||
cmdHelpRestore
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
;;
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
;;
|
||||
muplugin)
|
||||
printTitle " Malware MU 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)
|
||||
printTitle " Shell $@"
|
||||
cmdShell "$@"
|
||||
;;
|
||||
--log)
|
||||
printTitle " Log $@"
|
||||
cmdLog "$@"
|
||||
;;
|
||||
--describe)
|
||||
printTitle " Describe $@"
|
||||
cmdDescribe "$@"
|
||||
;;
|
||||
--delete)
|
||||
printTitle " Delete $@ $fontRed[DANGER]$fontReset"
|
||||
cmdDelete "$@"
|
||||
;;
|
||||
-b|--backup) cmdBackup "$@" ;;
|
||||
--restore) cmdRestore "$@" ;;
|
||||
--storage) cmdStorage "$@" ;;
|
||||
--script) cmdScript "$@" ;;
|
||||
--install) cmdInstall "$@" ;;
|
||||
--test) cmdTest "$@" ;;
|
||||
--malware) cmdMalware "$@" ;;
|
||||
--help) cmdHelp "$@" ;;
|
||||
dev) appDev "$@" ;;
|
||||
esac
|
||||
|
||||
local status=$?
|
||||
printFooter
|
||||
return "$status"
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
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
|
||||
}
|
||||
|
||||
# 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() {
|
||||
local option="$1"
|
||||
|
||||
printText "$logPath/$option/${appDate}_$appTime.log"
|
||||
printRow
|
||||
|
||||
printFile="$logPath/$option/${appDate}_$appTime.log"
|
||||
|
||||
local logFileOld
|
||||
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"
|
||||
;;
|
||||
*)
|
||||
printFile=""
|
||||
logFile="$logFileOld"
|
||||
|
||||
printTitle " $scriptLabel"
|
||||
printRow
|
||||
printWarning "Unsupported or empty command: $action"
|
||||
cmdHelpScript
|
||||
;;
|
||||
esac
|
||||
|
||||
printFile=""
|
||||
logFile="$logFileOld"
|
||||
printRow
|
||||
}
|
||||
@@ -0,0 +1,860 @@
|
||||
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
|
||||
|
||||
sourceFiles="${1:-$appAssetsPath/vhost/default}"
|
||||
shift
|
||||
|
||||
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
|
||||
|
||||
printRow;
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
mode="$1"
|
||||
shift
|
||||
|
||||
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
|
||||
|
||||
printRow;
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow;
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
sourceFiles="${1:-$appAssetsPath/vhost/wordpress}"
|
||||
if [[ ! -e "$sourceFiles" ]]; then
|
||||
sourceFiles="$appAssetsPath/vhost/wordpress.tar.gz"
|
||||
fi
|
||||
shift
|
||||
|
||||
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
|
||||
|
||||
printRow;
|
||||
}
|
||||
|
||||
# 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 openlitespeedConfigRebuild "$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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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 openlitespeedVhostList; 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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 openlitespeedVhostList; 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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"
|
||||
printRow
|
||||
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 openlitespeedVhostQuota "$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 "$openlitespeedVhostsPath/$domain.conf" ]]; then
|
||||
printDotText "file$fontReset" "$fontRed$openlitespeedVhostsPath/$domain.conf$fontReset"
|
||||
printDotText "file$fontReset" "${fontRed}not found$fontReset"
|
||||
else
|
||||
printDotText "file$fontReset" "$fontGreen$openlitespeedVhostsPath/$domain.conf$fontReset"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$vhostsPath/$domain" ]]; then
|
||||
printDotText "path$fontReset" "$fontRed$vhostsPath/$domain$fontReset"
|
||||
printDotText "path$fontReset" "${fontRed}not found$fontReset"
|
||||
else
|
||||
printDotText "path$fontReset" "$fontGreen$vhostsPath/$domain$fontReset"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$openlitespeedVhostDataPath/$domain" ]]; then
|
||||
printDotText "data$fontReset" "$fontRed$openlitespeedVhostDataPath/$domain$fontReset"
|
||||
printDotText "data$fontReset" "${fontRed}not found$fontReset"
|
||||
else
|
||||
printDotText "data$fontReset" "$fontGreen$openlitespeedVhostDataPath/$domain$fontReset"
|
||||
fi
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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 openlitespeedVhostQuota "$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="$vhostsPath/$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="$vhostsPath/$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="$vhostsPath/$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="$openlitespeedVhostDataPath/$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="$openlitespeedVhostDataPath/$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 openlitespeedVhostList; 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 "$openlitespeedVhostsPath/$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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
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 "$vhostsPath/$domain" "755:root:root"
|
||||
if ! run output error fileSignatureDiff "$vhostsPath/$domain" "755:root:root"; then
|
||||
printDanger "$error"
|
||||
elif [[ -n "$output" ]]; then
|
||||
local prefix=":$vhostsPath/$domain"
|
||||
printDot "$dots" "${fontCyan}vhost d 755:root:root$fontReset" "" "${output/$prefix/ /}"
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$vhostsPath/$domain/www" "d" "2750|$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$vhostsPath/$domain/www" "d" "2750|$ug:$ug"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}www d 2750:u:g$fontReset"
|
||||
local prefix=":$vhostsPath/$domain/www/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$vhostsPath/$domain/www" "f" "(600|640):$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$vhostsPath/$domain/www" "f" "(600|640):$ug:$ug"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}www f (600|640):u:g$fontReset"
|
||||
local prefix=":$vhostsPath/$domain/www/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$vhostsPath/$domain/tmp" "d" "770:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$vhostsPath/$domain/tmp" "d" "770:$ug:$ug"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}tmp d 770:u:g$fontReset"
|
||||
local prefix=":$vhostsPath/$domain/tmp/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$vhostsPath/$domain/tmp" "f" "660:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$vhostsPath/$domain/tmp" "f" "660:$ug:$ug"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}tmp f 660:u:g$fontReset"
|
||||
local prefix=":$vhostsPath/$domain/tmp/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$vhostsPath/$domain/session" "d" "770:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$vhostsPath/$domain/session" "d" "770:$ug:$ug"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}session d 770:u:g$fontReset"
|
||||
local prefix=":$vhostsPath/$domain/session/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$vhostsPath/$domain/session" "f" "(660|600):$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$vhostsPath/$domain/session" "f" "(660|600):$ug:$ug"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}session f (660|600):u:g$fontReset"
|
||||
local prefix=":$vhostsPath/$domain/session/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$openlitespeedVhostDataPath/$domain" "d" "750:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$openlitespeedVhostDataPath/$domain" "d" "750:$ug:$ug"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}data d 750:u:g$fontReset"
|
||||
local prefix=":$openlitespeedVhostDataPath/$domain/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# fileSignatureDiffList "$openlitespeedVhostDataPath/$domain" "f" "600:$ug:$ug"
|
||||
if ! run output error fileSignatureDiffList "$openlitespeedVhostDataPath/$domain" "f" "600:$ug:$ug"; then
|
||||
printDanger "$error"
|
||||
else
|
||||
lineCount=$(grep -c . <<< "$output" || true)
|
||||
if [[ "$lineCount" -gt 0 ]]; then
|
||||
local label="${fontCyan}data f 600:u:g$fontReset"
|
||||
local prefix=":$openlitespeedVhostDataPath/$domain/"
|
||||
while read -r line; do
|
||||
printDot "$dots" "$label" "" "${line/$prefix/ /}"
|
||||
done < <(awk 'NF' <<< "$output")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
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")
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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 "Process" "$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 "Process" "$labelFail"
|
||||
printDanger "$error"
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
*)
|
||||
printDotText "Type" "$type $labelUnknown"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
printDotText "Process" "$labelDone"
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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"
|
||||
printRow
|
||||
return 1
|
||||
fi
|
||||
printDotText "$selected" "$labelDone"
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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")
|
||||
|
||||
printRow
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
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 || { printRow; return 1; }
|
||||
|
||||
grep -Fxq -- "$selected" <(printf '%s\n' "${cronJobs[@]}") || {
|
||||
printDanger "Cannot add unmanaged job"
|
||||
printRow
|
||||
return 1
|
||||
}
|
||||
|
||||
if grep -Fxq -- "$selected" <<< "$jobList"; then
|
||||
printSuccess "Job already exists in cron"
|
||||
printRow
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp) || { printRow; return 1; }
|
||||
{ printf '%s\n' "$jobList"; printf '%s\n' "$selected"; } > "$tmp"
|
||||
if crontab -u root "$tmp"; then
|
||||
printSuccess "Successfully added job to cron"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
printDanger "Failed to add job to cron"
|
||||
printRow
|
||||
return 1
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Interactively selects and removes a managed cron job from root crontab.
|
||||
function cmdCronRemove() {
|
||||
local selected jobList
|
||||
cmdCronSelect selected jobList || { printRow; return 1; }
|
||||
|
||||
if ! grep -Fxq -- "$selected" <<< "$jobList"; then
|
||||
printSuccess "Job not found in cron"
|
||||
printRow
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp) || { printRow; return 1; }
|
||||
grep -Fxv -- "$selected" <<< "$jobList" > "$tmp"
|
||||
if crontab -u root "$tmp"; then
|
||||
printSuccess "Successfully removed job from cron"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
printDanger "Failed to remove job from cron"
|
||||
printRow
|
||||
return 1
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# Shows managed cron jobs; installed entries in green, missing in gray, unknown in yellow.
|
||||
function cmdCronList() {
|
||||
local jobList error
|
||||
run jobList error systemCronList || { printDanger "systemCronList: $error"; return 1; }
|
||||
|
||||
local i job fontColor
|
||||
printRow
|
||||
for ((i=0; i<${#cronJobs[@]}; i++)); do
|
||||
grep -Fxq -- "${cronJobs[$i]}" <<< "$jobList" && fontColor="$fontGreen" || fontColor="$fontGray"
|
||||
printf "%3d: %s\n" "$((i+1))" "$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
|
||||
((i++))
|
||||
printf "%3d: %s\n" "$i" "$fontYellow$job$fontReset"
|
||||
done <<< "$jobList"
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# [WARNING] Patches sshd_config to enable SFTP via internal-sftp with group-based chroot.
|
||||
function cmdSftpAddingSupport() {
|
||||
local error
|
||||
|
||||
printRow
|
||||
|
||||
if ! runError error sftpAddingSupport; then
|
||||
printDotText "SFTP adding support" "$labelFail"
|
||||
printDanger "$error"
|
||||
else
|
||||
printDotText "SFTP adding support" "$labelDone"
|
||||
fi
|
||||
|
||||
printRow
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,246 @@
|
||||
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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedVhostList; 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 openlitespeedVhostList; 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
function cmdWordpressCheck() {
|
||||
local target="$1"
|
||||
local vhostList error
|
||||
|
||||
printRow
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
printDanger "Target not specified";
|
||||
elif ! run vhostList error openlitespeedVhostList; 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
|
||||
|
||||
printRow
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 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 --> /var/www/vhosts/$domain/www"
|
||||
wordpressSearchReplace "$domain" "$abspathOld" "/var/www/vhosts/$domain/www"
|
||||
fi
|
||||
|
||||
printInfo "$wordpressLabel Replace in DB (7): $hostOld --> $domain"
|
||||
wordpressSearchReplace "$domain" "$hostOld" "$domain"
|
||||
|
||||
printInfo "$wordpressLabel Delete options: upload_path/upload_url_path..."
|
||||
wordpressExec "$domain" option delete upload_path || true
|
||||
wordpressExec "$domain" option delete upload_url_path || true
|
||||
|
||||
printInfo "$wordpressLabel Clean cache..."
|
||||
wordpressExec "$domain" transient delete --all || true
|
||||
|
||||
printInfo "$wordpressLabel Redis clean..."
|
||||
redisDomainClean "$domain" || true
|
||||
}
|
||||
@@ -0,0 +1,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 openlitespeedVhostList; 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" "$vhostsPath/$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" "$vhostsPath/$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")
|
||||
}
|
||||
@@ -0,0 +1,827 @@
|
||||
# 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; }
|
||||
}
|
||||
|
||||
# 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"
|
||||
[[ -n "$path" ]] || { appError "Path not specified"; return 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 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 }'
|
||||
}
|
||||
|
||||
# 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"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ===============================================================================================================
|
||||
# ===============================================================================================================
|
||||
# ===============================================================================================================
|
||||
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# DEPRECATED
|
||||
# Retrieves a list of files in an FTP directory along with their sizes (in ftpPath).
|
||||
#
|
||||
# $1 (assoc): An associative array where file names and their sizes will be saved.
|
||||
# $2 (path): The path to the directory on the FTP server.
|
||||
#
|
||||
# If a file is a directory, its size is set to "-1". If it's a regular file, its size is saved.
|
||||
function ftpEntrySizeList() {
|
||||
local -n assoc=$1
|
||||
local path="$2"
|
||||
|
||||
if ! run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$path/" --connect-timeout 10; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local line file size
|
||||
while IFS= read -r line; do
|
||||
file=$(echo "$line" | awk '{print $NF}')
|
||||
size=$(echo "$line" | awk '{print $5}')
|
||||
if [[ -n "$file" ]] && [[ "$line" =~ ^d.* ]]; then
|
||||
assoc["$file"]="-1"
|
||||
elif [[ -n "$file" ]] && [[ -n "$size" ]]; then
|
||||
assoc["$file"]="$size"
|
||||
fi
|
||||
done <<< "$output"
|
||||
}
|
||||
|
||||
# DEPRECATED
|
||||
# Retrieves a list of files and their sizes from a remote directory via SSH (in sshPath).
|
||||
#
|
||||
# $1 (assoc): An associative array where file names and their sizes will be saved.
|
||||
# $2 (path): The path to the remote directory where file sizes need to be retrieved.
|
||||
function sshEntrySizeList() {
|
||||
local -n assoc=$1
|
||||
local path="$2"
|
||||
|
||||
if ! run output error ssh -i "$sshKeyFile" "$sshUser@$sshHost" "du -ab --max-depth=1 $sshPath$path"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
output=$(echo "$output" | grep -v "[[:space:]]\+$sshPath$path$")
|
||||
|
||||
local size file
|
||||
while IFS=$'\t' read -r size file; do
|
||||
file=$(basename "$file")
|
||||
if [[ -n "$file" && -n "$size" ]]; then
|
||||
assoc["$file"]="$size"
|
||||
fi
|
||||
done <<< "$output"
|
||||
}
|
||||
|
||||
# DEPRECATED
|
||||
# Retrieves a list of files and their sizes in a specified directory on extended storage.
|
||||
#
|
||||
# $1 (assoc): An associative array where file names and their sizes will be saved.
|
||||
# $2 (path): The path to the local directory where file sizes need to be retrieved.
|
||||
function storageEntrySizeList() {
|
||||
local -A storageFunc=(
|
||||
["ftp"]=ftpEntrySizeList
|
||||
["rsync"]=ftpEntrySizeList
|
||||
["ssh"]=sshEntrySizeList
|
||||
)
|
||||
if [[ -z "${storageFunc[$storageType]}" ]]; then
|
||||
appError "Unknown value storageType: $storageType"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! run output error "${storageFunc[$storageType]}" "$@"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# DEPRECATED
|
||||
# Retrieves a list of files and their sizes in a specified local directory (in backupPath).
|
||||
#
|
||||
# $1 (assoc): An associative array where file names and their sizes will be saved.
|
||||
# $2 (path): The path to the local directory where file sizes need to be retrieved.
|
||||
function backupEntrySizeList() {
|
||||
local -n assoc=$1
|
||||
local path="$2"
|
||||
|
||||
if ! run output error du -ab --max-depth=1 "$backupDailyPath$path"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
local entryList
|
||||
entryList=$(echo "$output" | grep -v "[[:space:]]\+$backupDailyPath$path$")
|
||||
|
||||
local file size
|
||||
while IFS=$'\t' read -r size file; do
|
||||
file=$(basename "$file")
|
||||
if [[ -n "$file" && -n "$size" ]]; then
|
||||
assoc["$file"]="$size"
|
||||
fi
|
||||
done <<< "$entryList"
|
||||
}
|
||||
|
||||
|
||||
# DEPRECATED
|
||||
# List entries in a backup daily directory path.
|
||||
# $1 (path): sub-path within backupDailyPath to list.
|
||||
# [$2] (type): entry type filter (f: files, d: directories).
|
||||
function backupEntryList() {
|
||||
if ! run output error fileList "$backupDailyPath$1" "$2"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$output"
|
||||
}
|
||||
|
||||
function fileSignatureDiffList() {
|
||||
local path="$1" type="$2" mask="$3"
|
||||
if [[ -z "$path" || -z "$type" || -z "$mask" ]]; then
|
||||
printDanger "fileSignatureDiffList: missing argument(s)"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -d "$path" ]]; then
|
||||
printDanger "fileSignatureDiffList: path not found: $path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
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" ]] || { printDanger "Missing argument(s)"; return 1; }
|
||||
[[ -d "$path" ]] || { printDanger "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" ]]
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
# 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
|
||||
|
||||
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}"
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
my ($file, $mode, @args) = @ARGV;
|
||||
|
||||
defined $file && length $file or die "usage: $0 <file> <mode> ...\n";
|
||||
defined $mode && length $mode or die "mode is required\n";
|
||||
|
||||
sub mask_to_re {
|
||||
my ($mask) = @_;
|
||||
return undef if !defined($mask) || $mask eq '';
|
||||
$mask = quotemeta($mask);
|
||||
$mask =~ s/\\\*/.*/g;
|
||||
return qr/\A$mask\z/;
|
||||
}
|
||||
|
||||
sub parse_kv_line {
|
||||
my ($line, $wanted_key) = @_;
|
||||
return unless $line =~ /^(\h*)\Q$wanted_key\E(?:\h+(.*?))?\h*(?:\R)?$/;
|
||||
my $indent = $1;
|
||||
my $value = defined($2) ? $2 : '';
|
||||
return ($indent, $value);
|
||||
}
|
||||
|
||||
sub line_matches_delete {
|
||||
my ($line, $wanted_key, $value_re) = @_;
|
||||
my @m = parse_kv_line($line, $wanted_key) or return 0;
|
||||
return 1 unless $value_re;
|
||||
return $m[1] =~ $value_re;
|
||||
}
|
||||
|
||||
sub line_matches_exact {
|
||||
my ($line, $wanted_key, $wanted_value) = @_;
|
||||
my @m = parse_kv_line($line, $wanted_key) or return 0;
|
||||
return $m[1] eq $wanted_value;
|
||||
}
|
||||
|
||||
sub fmt_line {
|
||||
my ($indent, $k, $v) = @_;
|
||||
return sprintf("%s%-23s %s\n", $indent, $k, $v);
|
||||
}
|
||||
|
||||
sub brace_delta {
|
||||
my ($line) = @_;
|
||||
my $o = () = $line =~ /\{/g;
|
||||
my $c = () = $line =~ /\}/g;
|
||||
return $o - $c;
|
||||
}
|
||||
|
||||
sub find_section {
|
||||
my ($lines, $re) = @_;
|
||||
|
||||
for (my $i = 0; $i <= $#$lines; $i++) {
|
||||
next unless $lines->[$i] =~ /^(\h*)$re\h*\{\h*(?:\R)?$/;
|
||||
|
||||
my $indent = $1 . ' ';
|
||||
my $depth = 1;
|
||||
|
||||
for (my $j = $i + 1; $j <= $#$lines; $j++) {
|
||||
$depth += brace_delta($lines->[$j]);
|
||||
if ($depth == 0) {
|
||||
return ($i, $j, $indent);
|
||||
}
|
||||
}
|
||||
|
||||
die "section '$re' is not closed\n";
|
||||
}
|
||||
|
||||
die "section '$re' not found\n";
|
||||
}
|
||||
|
||||
sub delete_key {
|
||||
my ($lines, $section_re, $key, $mask) = @_;
|
||||
my $value_re = mask_to_re($mask);
|
||||
|
||||
if ($section_re eq '') {
|
||||
my @out;
|
||||
my $depth = 0;
|
||||
|
||||
for my $line (@$lines) {
|
||||
my $remove = ($depth == 0 && line_matches_delete($line, $key, $value_re)) ? 1 : 0;
|
||||
push @out, $line unless $remove;
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
@$lines = @out;
|
||||
return;
|
||||
}
|
||||
|
||||
my ($start, $end, undef) = find_section($lines, $section_re);
|
||||
|
||||
my @out;
|
||||
push @out, @$lines[0 .. $start];
|
||||
|
||||
my $depth = 1;
|
||||
for my $i ($start + 1 .. $end - 1) {
|
||||
my $line = $lines->[$i];
|
||||
my $remove = ($depth == 1 && line_matches_delete($line, $key, $value_re)) ? 1 : 0;
|
||||
push @out, $line unless $remove;
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
push @out, @$lines[$end .. $#$lines];
|
||||
@$lines = @out;
|
||||
}
|
||||
|
||||
sub add_key {
|
||||
my ($lines, $section_re, $key, $value) = @_;
|
||||
die "value is required for add\n" if !defined($value) || $value eq '';
|
||||
|
||||
if ($section_re eq '') {
|
||||
my $depth = 0;
|
||||
|
||||
for my $line (@$lines) {
|
||||
if ($depth == 0 && line_matches_exact($line, $key, $value)) {
|
||||
return;
|
||||
}
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
my $new = fmt_line('', $key, $value);
|
||||
|
||||
my $root_pos;
|
||||
my $first_section_pos;
|
||||
$depth = 0;
|
||||
|
||||
for (my $i = 0; $i <= $#$lines; $i++) {
|
||||
my $line = $lines->[$i];
|
||||
|
||||
if ($depth == 0) {
|
||||
$root_pos = $i
|
||||
if !defined($root_pos) && $line =~ /^\h*serverName\h+\S*/;
|
||||
|
||||
$first_section_pos = $i
|
||||
if !defined($first_section_pos) && $line =~ /^\h*\S.*\{\h*(?:\R)?$/;
|
||||
}
|
||||
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
if (defined $root_pos) {
|
||||
splice @$lines, $root_pos + 1, 0, $new;
|
||||
} elsif (defined $first_section_pos) {
|
||||
splice @$lines, $first_section_pos, 0, $new;
|
||||
} else {
|
||||
push @$lines, $new;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
my ($start, $end, $indent) = find_section($lines, $section_re);
|
||||
|
||||
my $depth = 1;
|
||||
for my $i ($start + 1 .. $end - 1) {
|
||||
my $line = $lines->[$i];
|
||||
if ($depth == 1 && line_matches_exact($line, $key, $value)) {
|
||||
return;
|
||||
}
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
my $new = fmt_line($indent, $key, $value);
|
||||
splice @$lines, $end, 0, $new;
|
||||
}
|
||||
|
||||
sub build_vhost_section {
|
||||
my ($domain) = @_;
|
||||
return if !defined($domain) || $domain eq '';
|
||||
|
||||
my @out;
|
||||
push @out, "virtualhost $domain {\n";
|
||||
push @out, fmt_line(' ', 'vhRoot', "/var/www/vhosts/$domain");
|
||||
push @out, fmt_line(' ', 'configFile', "/usr/local/lsws/conf/vhosts/$domain.conf");
|
||||
push @out, fmt_line(' ', 'allowSymbolLink', '1');
|
||||
push @out, fmt_line(' ', 'enableScript', '1');
|
||||
push @out, fmt_line(' ', 'restrained', '1');
|
||||
push @out, fmt_line(' ', 'setUIDMode', '2');
|
||||
push @out, "}\n";
|
||||
|
||||
return join('', @out);
|
||||
}
|
||||
|
||||
sub has_vhost_section {
|
||||
my ($lines, $name) = @_;
|
||||
|
||||
for my $line (@$lines) {
|
||||
return 1 if $line =~ /^\h*virtualhost\h+\Q$name\E\h*\{/;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub add_vhost_section {
|
||||
my ($lines, $name) = @_;
|
||||
|
||||
return if !defined($name) || $name eq '';
|
||||
return if has_vhost_section($lines, $name);
|
||||
|
||||
my @section = split /(?<=\n)/, build_vhost_section($name), -1;
|
||||
pop @section if @section && $section[-1] eq '';
|
||||
|
||||
if (@$lines && $lines->[-1] !~ /\n\z/) {
|
||||
$lines->[-1] .= "\n";
|
||||
}
|
||||
|
||||
push @$lines, "\n" if @$lines && $lines->[-1] !~ /^\s*$/;
|
||||
push @$lines, @section;
|
||||
}
|
||||
|
||||
sub delete_named_section {
|
||||
my ($lines, $header_re) = @_;
|
||||
|
||||
my @out;
|
||||
my $deleted = 0;
|
||||
|
||||
for (my $i = 0; $i <= $#$lines; $i++) {
|
||||
my $line = $lines->[$i];
|
||||
|
||||
if ($line =~ /^\h*$header_re\h*\{\h*(?:\R)?$/) {
|
||||
my $depth = 1;
|
||||
$deleted = 1;
|
||||
|
||||
for ($i = $i + 1; $i <= $#$lines; $i++) {
|
||||
$depth += brace_delta($lines->[$i]);
|
||||
last if $depth == 0;
|
||||
}
|
||||
|
||||
next;
|
||||
}
|
||||
|
||||
push @out, $line;
|
||||
}
|
||||
|
||||
@$lines = @out;
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
sub get_suspended_vhosts {
|
||||
my ($lines) = @_;
|
||||
|
||||
my $depth = 0;
|
||||
for my $line (@$lines) {
|
||||
if ($depth == 0) {
|
||||
my @m = parse_kv_line($line, 'suspendedVhosts');
|
||||
if (@m) {
|
||||
return grep { length }
|
||||
map { s/^\h+|\h+$//gr }
|
||||
split /,/, $m[1];
|
||||
}
|
||||
}
|
||||
$depth += brace_delta($line);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub set_suspended_vhosts {
|
||||
my ($lines, @list) = @_;
|
||||
|
||||
@list = grep { defined($_) && $_ ne '' } @list;
|
||||
|
||||
delete_key($lines, '', 'suspendedVhosts', '');
|
||||
|
||||
if (@list) {
|
||||
add_key($lines, '', 'suspendedVhosts', join(',', @list));
|
||||
}
|
||||
}
|
||||
|
||||
sub suspend_vhost {
|
||||
my ($lines, $domain) = @_;
|
||||
return if !defined($domain) || $domain eq '';
|
||||
|
||||
my @current = get_suspended_vhosts($lines);
|
||||
|
||||
my @new = ($domain);
|
||||
for my $item (@current) {
|
||||
push @new, $item if $item ne $domain;
|
||||
}
|
||||
|
||||
set_suspended_vhosts($lines, @new);
|
||||
}
|
||||
|
||||
sub unsuspend_vhost {
|
||||
my ($lines, $domain) = @_;
|
||||
return if !defined($domain) || $domain eq '';
|
||||
|
||||
my @current = get_suspended_vhosts($lines);
|
||||
my @new = grep { $_ ne $domain } @current;
|
||||
|
||||
set_suspended_vhosts($lines, @new);
|
||||
}
|
||||
|
||||
sub get_listener_http_maps {
|
||||
my ($lines) = @_;
|
||||
|
||||
my ($start, $end, undef) = find_section($lines, 'listener\h+HTTP');
|
||||
|
||||
my @maps;
|
||||
|
||||
for my $i ($start + 1 .. $end - 1) {
|
||||
my $clean = $lines->[$i];
|
||||
$clean =~ s/#.*$//;
|
||||
$clean =~ s/^\s+|\s+$//g;
|
||||
next if $clean eq '';
|
||||
|
||||
if ($clean =~ /^map\s+(\S+)\s+(\S+)$/i) {
|
||||
push @maps, [$1, $2];
|
||||
}
|
||||
}
|
||||
|
||||
return @maps;
|
||||
}
|
||||
|
||||
sub vhost_list {
|
||||
my ($lines, $type) = @_;
|
||||
$type //= 'all';
|
||||
|
||||
die "unknown vhost list type '$type'\n"
|
||||
if $type !~ /\A(?:all|up|down)\z/;
|
||||
|
||||
my @all;
|
||||
my %down = map { $_ => 1 } get_suspended_vhosts($lines);
|
||||
|
||||
for my $line (@$lines) {
|
||||
my $clean = $line;
|
||||
$clean =~ s/#.*$//;
|
||||
$clean =~ s/^\s+|\s+$//g;
|
||||
next if $clean eq '';
|
||||
|
||||
if ($clean =~ /^virtualhost\s+([^\s\{]+)\s*\{?/i) {
|
||||
push @all, $1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($type eq 'all') {
|
||||
print "$_\n" for @all;
|
||||
} elsif ($type eq 'down') {
|
||||
print "$_\n" for grep { exists $down{$_} } @all;
|
||||
} elsif ($type eq 'up') {
|
||||
print "$_\n" for grep { !exists $down{$_} } @all;
|
||||
}
|
||||
}
|
||||
|
||||
sub vhost_list_map {
|
||||
my ($lines, $type) = @_;
|
||||
$type //= 'all';
|
||||
|
||||
die "unknown vhost map list type '$type'\n"
|
||||
if $type !~ /\A(?:all|up|down)\z/;
|
||||
|
||||
my %down = map { $_ => 1 } get_suspended_vhosts($lines);
|
||||
my %seen;
|
||||
|
||||
for my $map (get_listener_http_maps($lines)) {
|
||||
my ($vhost, $domain) = @$map;
|
||||
|
||||
next if $vhost ne $domain;
|
||||
next if $seen{$vhost}++;
|
||||
|
||||
if ($type eq 'down') {
|
||||
next if !exists $down{$vhost};
|
||||
} elsif ($type eq 'up') {
|
||||
next if exists $down{$vhost};
|
||||
}
|
||||
|
||||
print "$vhost\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub alias_list {
|
||||
my ($lines) = @_;
|
||||
|
||||
my %seen;
|
||||
|
||||
for my $map (get_listener_http_maps($lines)) {
|
||||
my ($vhost, $domain) = @$map;
|
||||
|
||||
next if $vhost eq $domain;
|
||||
next if $seen{$domain}++;
|
||||
|
||||
print "$domain\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub vhost_alias {
|
||||
my ($lines, $name) = @_;
|
||||
|
||||
die "virtual host name is required\n"
|
||||
if !defined($name) || $name eq '';
|
||||
|
||||
my %seen;
|
||||
|
||||
for my $map (get_listener_http_maps($lines)) {
|
||||
my ($vhost, $domain) = @$map;
|
||||
|
||||
next if $vhost ne $name;
|
||||
next if $domain eq $name;
|
||||
next if $seen{$domain}++;
|
||||
|
||||
print "$domain\n";
|
||||
}
|
||||
}
|
||||
|
||||
open my $fh, '<', $file or die "cannot read '$file': $!\n";
|
||||
local $/;
|
||||
my $content = <$fh>;
|
||||
close $fh;
|
||||
|
||||
my @L = split /(?<=\n)/, $content, -1;
|
||||
|
||||
if ($mode eq 'del') {
|
||||
my ($section_re, $key, $mask) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
$mask //= '';
|
||||
delete_key(\@L, $section_re, $key, $mask);
|
||||
}
|
||||
elsif ($mode eq 'add') {
|
||||
my ($section_re, $key, $value) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $value or die "value is required\n";
|
||||
add_key(\@L, $section_re, $key, $value);
|
||||
}
|
||||
elsif ($mode eq 'set') {
|
||||
my ($section_re, $key, $value) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $value or die "value is required\n";
|
||||
delete_key(\@L, $section_re, $key, '');
|
||||
add_key(\@L, $section_re, $key, $value);
|
||||
}
|
||||
elsif ($mode eq 'set_masked') {
|
||||
my ($section_re, $key, $mask, $value) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $mask or die "mask is required\n";
|
||||
defined $value or die "value is required\n";
|
||||
delete_key(\@L, $section_re, $key, $mask);
|
||||
add_key(\@L, $section_re, $key, $value);
|
||||
}
|
||||
elsif ($mode eq 'del_masked') {
|
||||
my ($section_re, $key, $mask) = @args;
|
||||
defined $section_re or die "section_re is required\n";
|
||||
defined $key or die "key is required\n";
|
||||
defined $mask or die "mask is required\n";
|
||||
delete_key(\@L, $section_re, $key, $mask);
|
||||
}
|
||||
elsif ($mode eq 'vhost_add') {
|
||||
my ($name) = @args;
|
||||
defined $name && length $name or die "virtual host name is required\n";
|
||||
add_vhost_section(\@L, $name);
|
||||
}
|
||||
elsif ($mode eq 'vhost_del') {
|
||||
my ($name) = @args;
|
||||
defined $name && length $name or die "virtual host name is required\n";
|
||||
my $quoted = quotemeta($name);
|
||||
delete_named_section(\@L, "virtualhost\\h+$quoted");
|
||||
}
|
||||
elsif ($mode eq 'vhost_down') {
|
||||
my ($name) = @args;
|
||||
defined $name && length $name or die "virtual host name is required\n";
|
||||
suspend_vhost(\@L, $name);
|
||||
}
|
||||
elsif ($mode eq 'vhost_up') {
|
||||
my ($name) = @args;
|
||||
defined $name && length $name or die "virtual host name is required\n";
|
||||
unsuspend_vhost(\@L, $name);
|
||||
}
|
||||
elsif ($mode eq 'vhost_list') {
|
||||
my ($type) = @args;
|
||||
vhost_list(\@L, $type // 'all');
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'vhost_list_map') {
|
||||
my ($type) = @args;
|
||||
vhost_list_map(\@L, $type // 'all');
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'alias_list') {
|
||||
alias_list(\@L);
|
||||
exit 0;
|
||||
}
|
||||
elsif ($mode eq 'vhost_alias') {
|
||||
my ($name) = @args;
|
||||
vhost_alias(\@L, $name);
|
||||
exit 0;
|
||||
}
|
||||
else {
|
||||
die "unknown mode '$mode'\n";
|
||||
}
|
||||
|
||||
$content = join '', @L;
|
||||
|
||||
open my $out, '>', $file or die "cannot write '$file': $!\n";
|
||||
print {$out} $content;
|
||||
close $out or die "cannot close '$file': $!\n";
|
||||
|
||||
exit 0;
|
||||
@@ -0,0 +1,134 @@
|
||||
backupDailyDirPattern="^20[0-9]{2}-[0-9]{2}-[0-9]{2}$backupDailySuffix$"
|
||||
backupArchiveDirPattern="^20[0-9]{2}-[0-9]{2}-[0-9]{2}$backupArchiveSuffix$"
|
||||
|
||||
# Generates a backup destination path; uses $backupFirst for the first backup of the day, $appTime otherwise.
|
||||
# [$1] (path): explicit destination path; returned unchanged if provided.
|
||||
function backupPathGenerate() {
|
||||
local path="$1"
|
||||
|
||||
if [[ -z "$path" ]]; then
|
||||
path="$backupDailyPath/$appDate/$backupFirstDir"
|
||||
[[ -d "$path" ]] && path="$backupDailyPath/$appDate/$appTime"
|
||||
fi
|
||||
|
||||
printf '%s' "$path"
|
||||
}
|
||||
|
||||
# Creates a file backup archive for a site.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (file): target archive file path.
|
||||
function backupSiteFiles() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local file="$2"
|
||||
[[ -n "$file" ]] || { appError "Target file not specified"; return 1; }
|
||||
|
||||
local compressProgram=""
|
||||
if [[ -n "${pigzThreads:-}" ]] && command -v pigz &>/dev/null; then
|
||||
compressProgram="pigz -p $pigzThreads"
|
||||
fi
|
||||
|
||||
archiveCreate "$vhostsPath/$domain" "$file" "$compressProgram"
|
||||
}
|
||||
|
||||
# Creates a database backup for a site.
|
||||
# $1 (domain): site domain name.
|
||||
# $2 (file): target database backup file path.
|
||||
function backupSiteDatabase() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local file="$2"
|
||||
[[ -n "$file" ]] || { appError "Target file not specified"; return 1; }
|
||||
|
||||
local databaseName databaseUser databasePass
|
||||
databaseName=$(siteConfigGet "$domain" "databaseName")
|
||||
[[ -n "$databaseName" ]] || { appError "databaseName not found or empty"; return 1; }
|
||||
databaseUser=$(siteConfigGet "$domain" "databaseUser")
|
||||
[[ -n "$databaseUser" ]] || { appError "databaseUser not found or empty"; return 1; }
|
||||
databasePass=$(siteConfigGet "$domain" "databasePass")
|
||||
[[ -n "$databasePass" ]] || { appError "databasePass not found or empty"; return 1; }
|
||||
|
||||
mariadbMasterExport "$databaseUser" "$databasePass" "$databaseName" "$file"
|
||||
}
|
||||
|
||||
# Creates a full backup for a site: files, database, and OLS vhost config.
|
||||
# $1 (domain): site domain name.
|
||||
# [$2] (path): destination directory; auto-generated if omitted.
|
||||
# [$3] (type): backup type: zip, tar, or archive (defaults to $backupType).
|
||||
function backupSite() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local path
|
||||
path=$(backupPathGenerate "$2")
|
||||
|
||||
local type="${3:-$backupType}"
|
||||
case "$type" in
|
||||
zip)
|
||||
backupSiteFiles "$domain" "$path/$domain.zip" || return 1
|
||||
backupSiteDatabase "$domain" "$path/$domain.sql.zip" || return 1
|
||||
;;
|
||||
tar|archive)
|
||||
backupSiteFiles "$domain" "$path/$domain.tar.gz" || return 1
|
||||
backupSiteDatabase "$domain" "$path/$domain.sql.tar.gz" || return 1
|
||||
;;
|
||||
*)
|
||||
appError "Unknown type: $type"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
cp -f -- "$openlitespeedVhostsPath/$domain.conf" "$path/$domain.conf" || {
|
||||
appError "Failed to copy vhost config: $domain"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Syncs a local path to a remote server using rsync.
|
||||
# $1 (sourcePath): local path to sync.
|
||||
function storagePathSyncRsync() {
|
||||
local sourcePath="$1"
|
||||
[[ -n "$sourcePath" ]] || { appError "Source path not specified"; return 1; }
|
||||
|
||||
rsync -aH --delete-after --partial --partial-dir=.rsync-partial --password-file="$rsyncPassFile" "$sourcePath" rsync://"$rsyncUri$rsyncPath"
|
||||
}
|
||||
|
||||
# Syncs a local path to a remote server over SSH using rsync.
|
||||
# $1 (sourcePath): local path to sync.
|
||||
function storagePathSyncSSH() {
|
||||
local sourcePath="$1"
|
||||
[[ -n "$sourcePath" ]] || { appError "Source path not specified"; return 1; }
|
||||
|
||||
rsync -aH --delete-after --partial --partial-dir=.rsync-partial -e "ssh -i $sshKeyFile" "$sourcePath" "$sshUser@$sshHost:$sshPath"
|
||||
}
|
||||
|
||||
function storageBackupRemove() {
|
||||
local backup error
|
||||
backup="$1"
|
||||
[[ -n "$backup" ]] || { appError "Backup not specified"; return 1; }
|
||||
|
||||
case "$storageType" in
|
||||
rsync)
|
||||
if ! runError error rsyncDirectoryClean "$rsyncPath/$backup"; then
|
||||
appError "$error"
|
||||
elif ! runError error ftpDirectoryDelete "/$backup"; then
|
||||
appError "$error"
|
||||
fi
|
||||
;;
|
||||
ssh)
|
||||
if ! runError error sshDirectoryDelete "/$backup"; then
|
||||
appError "$error"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
appError "Unknown storageType: $storageType"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,931 @@
|
||||
#!/bin/bash
|
||||
|
||||
diagLabel="[Diag]"
|
||||
diagExcludeVhostsRegex='localhost|Example'
|
||||
|
||||
# Writes a section header to the diag log file and prints it to the info log.
|
||||
# $1 (title): section title text.
|
||||
function diagLogSection() {
|
||||
local title="$1"
|
||||
local padding
|
||||
local logPath
|
||||
|
||||
logInfo "$diagLabel $title"
|
||||
|
||||
padding=$((72 - ${#title}))
|
||||
if (( padding > 0 )); then
|
||||
title="${title} $(printf '.%.s' $(seq 1 "$padding"))"
|
||||
fi
|
||||
|
||||
logPath=$(dirname "$diagFile")
|
||||
[[ -d "$logPath" ]] || mkdir -p "$logPath"
|
||||
|
||||
printf "[ %s ]\n" "$title" >> "$diagFile"
|
||||
}
|
||||
|
||||
# Runs a command and appends its output (stdout and stderr) to the diag file.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function diagCmd() {
|
||||
echo "+ $*" >> "$diagFile"
|
||||
"$@" >> "$diagFile" 2>&1 || true
|
||||
echo >> "$diagFile"
|
||||
}
|
||||
|
||||
# Runs a shell command via bash -lc and appends its output to the diag file; errors ignored.
|
||||
# $@ (...): shell command string to execute.
|
||||
function diagShTry() {
|
||||
echo "+ $*" >> "$diagFile"
|
||||
bash -lc "$*" >> "$diagFile" 2>&1 || true
|
||||
echo >> "$diagFile"
|
||||
}
|
||||
|
||||
# Runs a kubectl command via k3sRun and appends its output to the diag file; errors ignored.
|
||||
# $@ (...): kubectl arguments to pass to k3sRun.
|
||||
function diagK3sTry() {
|
||||
echo "+ k3sRun $*" >> "$diagFile"
|
||||
k3sRun "$@" >> "$diagFile" 2>&1 || true
|
||||
echo >> "$diagFile"
|
||||
}
|
||||
|
||||
# Converts $diagSince (e.g. 4h, 30m, 2d) to a human-readable date argument for the date command.
|
||||
function diagSinceDateArg() {
|
||||
local s="${diagSince:-4h}"
|
||||
|
||||
case "$s" in
|
||||
*m)
|
||||
printf '%s minutes ago' "${s%m}"
|
||||
;;
|
||||
*h)
|
||||
printf '%s hours ago' "${s%h}"
|
||||
;;
|
||||
*d)
|
||||
printf '%s days ago' "${s%d}"
|
||||
;;
|
||||
*)
|
||||
printf '4 hours ago'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Returns the Unix epoch timestamp corresponding to the $diagSince cutoff time.
|
||||
function diagCutoffEpoch() {
|
||||
date -d "$(diagSinceDateArg)" +%s 2>/dev/null || date -d "4 hours ago" +%s
|
||||
}
|
||||
|
||||
# Returns the HH:MM:SS start time for sar based on $diagSince; falls back to 00:00:00 if in the past.
|
||||
function diagSarStartTime() {
|
||||
local t now
|
||||
|
||||
t="$(date -d "$(diagSinceDateArg)" +%H:%M:%S 2>/dev/null || echo "00:00:00")"
|
||||
now="$(date +%H:%M:%S)"
|
||||
|
||||
if [[ "$t" > "$now" ]]; then
|
||||
printf "00:00:00"
|
||||
else
|
||||
printf "%s" "$t"
|
||||
fi
|
||||
}
|
||||
|
||||
# Writes diagnostic metadata (timestamp, since, hostname, file path) to the diag file.
|
||||
function diagMeta() {
|
||||
diagLogSection "Diagnostic metadata"
|
||||
|
||||
{
|
||||
echo "started_at: $(date -Iseconds)"
|
||||
echo "since: $diagSince"
|
||||
echo "hostname: [SERVER]"
|
||||
echo "file: $diagFile"
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Replaces hostnames, user paths, and UIDs in the diag file with redacted placeholders.
|
||||
function diagSanitizeReport() {
|
||||
local hostLower
|
||||
local hostFqdn
|
||||
local hostFqdnLower
|
||||
local sanitizeHosts
|
||||
|
||||
[[ -f "$diagFile" ]] || return 0
|
||||
|
||||
hostLower="$(printf '%s' "$hostName" | tr '[:upper:]' '[:lower:]')"
|
||||
hostFqdn="$(hostname -f 2>/dev/null || true)"
|
||||
hostFqdnLower="$(printf '%s' "$hostFqdn" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
sanitizeHosts="$(
|
||||
printf '%s\n' "$hostName" "$hostLower" "$hostFqdn" "$hostFqdnLower" | awk 'NF && !seen[$0]++'
|
||||
)"
|
||||
|
||||
SANITIZE_HOSTS="$sanitizeHosts" perl -0pi -e '
|
||||
my $hosts = $ENV{SANITIZE_HOSTS} // "";
|
||||
for my $h (split /\n/, $hosts) {
|
||||
next unless defined $h && length $h;
|
||||
my $q = quotemeta($h);
|
||||
s/$q/[SERVER]/g;
|
||||
}
|
||||
|
||||
s#(/media/)[^/\s]+#$1[USER]#g;
|
||||
s#(/run/user/)[0-9]+#$1[UID]#g;
|
||||
' "$diagFile"
|
||||
}
|
||||
|
||||
# Filters stdin lines, excluding those matching reserved vhost names ($diagExcludeVhostsRegex).
|
||||
function diagGrepExcludeReserved() {
|
||||
local regex="${diagExcludeVhostsRegex:-}"
|
||||
|
||||
if [[ -n "$regex" ]]; then
|
||||
grep -Eiv "$regex"
|
||||
else
|
||||
cat
|
||||
fi
|
||||
}
|
||||
|
||||
# Appends filtered disk usage (df -h, excluding overlay/tmpfs/pod mounts) to the diag file.
|
||||
function diagDfUsage() {
|
||||
{
|
||||
echo "+ df -h filtered"
|
||||
df -h \
|
||||
-x tmpfs \
|
||||
-x devtmpfs \
|
||||
-x overlay \
|
||||
-x squashfs \
|
||||
-x efivarfs \
|
||||
2>/dev/null | awk '
|
||||
NR == 1 { print; next }
|
||||
$6 ~ "^/run/k3s/" { next }
|
||||
$6 ~ "^/var/lib/kubelet/pods/" { next }
|
||||
$6 ~ "^/run/user/" { next }
|
||||
$6 ~ "^/media/" { next }
|
||||
{ print }
|
||||
'
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends filtered inode usage (df -i, excluding overlay/tmpfs/pod mounts) to the diag file.
|
||||
function diagDfInodes() {
|
||||
{
|
||||
echo "+ df -i filtered"
|
||||
df -i \
|
||||
-x tmpfs \
|
||||
-x devtmpfs \
|
||||
-x overlay \
|
||||
-x squashfs \
|
||||
-x efivarfs \
|
||||
2>/dev/null | awk '
|
||||
NR == 1 { print; next }
|
||||
$6 ~ "^/run/k3s/" { next }
|
||||
$6 ~ "^/var/lib/kubelet/pods/" { next }
|
||||
$6 ~ "^/run/user/" { next }
|
||||
$6 ~ "^/media/" { next }
|
||||
{ print }
|
||||
'
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends iostat extended stats (1s interval, 5 samples, loop/sr devices excluded) to the diag file.
|
||||
function diagIostat() {
|
||||
{
|
||||
echo "+ iostat -x -z 1 5 | filter loop/sr devices"
|
||||
|
||||
{
|
||||
iostat -x -z 1 5 2>/dev/null || iostat -x 1 5 2>/dev/null
|
||||
} | awk '
|
||||
/^loop[0-9]+/ { next }
|
||||
/^sr[0-9]+/ { next }
|
||||
{ print }
|
||||
'
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Runs a sar command and appends its output to the diag file; skips gracefully if data is unavailable.
|
||||
# $1 (title): section title for the diag log.
|
||||
# $2 (cmd): sar command string to execute.
|
||||
function diagSarTry() {
|
||||
local title="$1"
|
||||
local cmd="$2"
|
||||
local saFile
|
||||
|
||||
saFile="/var/log/sysstat/sa$(date +%d)"
|
||||
|
||||
diagLogSection "$title"
|
||||
|
||||
{
|
||||
echo "+ $cmd"
|
||||
|
||||
if [[ ! -r "$saFile" ]]; then
|
||||
echo "sar_data_available: no"
|
||||
echo "sar_file: $saFile"
|
||||
echo "hint: enable sysstat data collection if SAR trends are needed"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
bash -lc "$cmd" || true
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends a filtered k3s node summary (conditions, capacity, allocated resources) to the diag file.
|
||||
function diagK3sNodeSummary() {
|
||||
{
|
||||
echo "+ k3sRun describe node filtered"
|
||||
|
||||
echo
|
||||
echo "Conditions:"
|
||||
k3sRun describe node 2>/dev/null | awk '
|
||||
/^Conditions:/ { p=1; next }
|
||||
/^Addresses:/ { p=0 }
|
||||
p {
|
||||
if ($1 == "Type" ||
|
||||
$1 == "----" ||
|
||||
$1 == "MemoryPressure" ||
|
||||
$1 == "DiskPressure" ||
|
||||
$1 == "PIDPressure" ||
|
||||
$1 == "Ready") {
|
||||
print
|
||||
}
|
||||
}
|
||||
' || true
|
||||
|
||||
echo
|
||||
echo "Capacity/Allocatable:"
|
||||
k3sRun describe node 2>/dev/null | awk '
|
||||
/^Capacity:/ { p=1; print; next }
|
||||
/^System Info:/ { p=0 }
|
||||
p && $1 !~ /InternalIP|Hostname|Machine|UUID|Boot|ProviderID/ { print }
|
||||
' || true
|
||||
|
||||
echo
|
||||
echo "Allocated resources:"
|
||||
k3sRun describe node 2>/dev/null | awk '
|
||||
/^Allocated resources:/ { p=1; print; next }
|
||||
/^Events:/ { p=0 }
|
||||
p { print }
|
||||
' || true
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends a MariaDB slave replication status summary to the diag file.
|
||||
function diagMariaDBReplicaSummary() {
|
||||
local tmp
|
||||
|
||||
tmp="$(mktemp)"
|
||||
|
||||
diagLogSection "MariaDB slave replication summary"
|
||||
|
||||
{
|
||||
echo "+ mariadbSlaveRootQuery SHOW REPLICA STATUS"
|
||||
echo
|
||||
|
||||
mariadbSlaveRootQuery "SHOW REPLICA STATUS\G" > "$tmp" 2>&1 || true
|
||||
|
||||
if grep -q 'Slave_IO_Running:' "$tmp"; then
|
||||
awk -F': ' '
|
||||
/^[[:space:]]*Slave_IO_State:/ { print "Slave_IO_State:", $2 }
|
||||
/^[[:space:]]*Slave_IO_Running:/ { print "Slave_IO_Running:", $2 }
|
||||
/^[[:space:]]*Slave_SQL_Running:/ { print "Slave_SQL_Running:", $2 }
|
||||
/^[[:space:]]*Seconds_Behind_Master:/ { print "Seconds_Behind_Master:", $2 }
|
||||
/^[[:space:]]*Last_IO_Errno:/ { print "Last_IO_Errno:", $2 }
|
||||
/^[[:space:]]*Last_SQL_Errno:/ { print "Last_SQL_Errno:", $2 }
|
||||
/^[[:space:]]*Relay_Log_Space:/ { print "Relay_Log_Space:", $2 }
|
||||
/^[[:space:]]*Read_Master_Log_Pos:/ { print "Read_Master_Log_Pos:", $2 }
|
||||
/^[[:space:]]*Exec_Master_Log_Pos:/ { print "Exec_Master_Log_Pos:", $2 }
|
||||
/^[[:space:]]*Using_Gtid:/ { print "Using_Gtid:", $2 }
|
||||
/^[[:space:]]*Parallel_Mode:/ { print "Parallel_Mode:", $2 }
|
||||
/^[[:space:]]*Slave_SQL_Running_State:/ { print "Slave_SQL_Running_State:", $2 }
|
||||
' "$tmp" || true
|
||||
|
||||
echo
|
||||
rm -f "$tmp"
|
||||
return
|
||||
fi
|
||||
|
||||
: > "$tmp"
|
||||
mariadbSlaveRootQuery "SHOW REPLICA STATUS;" > "$tmp" 2>&1 || true
|
||||
|
||||
if [[ ! -s "$tmp" ]]; then
|
||||
echo "replication_status_available: no"
|
||||
echo
|
||||
rm -f "$tmp"
|
||||
return
|
||||
fi
|
||||
|
||||
awk -F'\t' '
|
||||
NF >= 12 {
|
||||
print "Slave_IO_State:", $1
|
||||
print "Master_Host: [REDACTED]"
|
||||
print "Master_User: [REDACTED]"
|
||||
print "Master_Port:", $4
|
||||
print "Master_Log_File:", $6
|
||||
print "Read_Master_Log_Pos:", $7
|
||||
print "Relay_Log_File:", $8
|
||||
print "Relay_Log_Pos:", $9
|
||||
print "Relay_Master_Log_File:", $10
|
||||
print "Slave_IO_Running:", $11
|
||||
print "Slave_SQL_Running:", $12
|
||||
found=1
|
||||
next
|
||||
}
|
||||
{
|
||||
print
|
||||
}
|
||||
END {
|
||||
if (!found) {
|
||||
print "replication_status_parse: raw fallback"
|
||||
}
|
||||
}
|
||||
' "$tmp" || true
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
# Parses the MariaDB slow query log since $diagSince and appends a fingerprint summary to the diag file.
|
||||
function diagMariaDBSlowSummary() {
|
||||
local mariadbMasterSlowLog="$mariadbMasterPath/slow.log"
|
||||
local cutoff
|
||||
|
||||
cutoff="$(diagCutoffEpoch)"
|
||||
|
||||
diagLogSection "MariaDB slow query summary only"
|
||||
|
||||
{
|
||||
echo "+ summarize slow log: $mariadbMasterSlowLog since ${diagSince:-4h}"
|
||||
echo
|
||||
|
||||
if [[ ! -r "$mariadbMasterSlowLog" ]]; then
|
||||
echo "slow_log_readable: no"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
perl -MTime::Local - "$mariadbMasterSlowLog" "$cutoff" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
use Time::Local;
|
||||
|
||||
my ($file, $cutoff) = @ARGV;
|
||||
|
||||
open my $fh, '<', $file or do {
|
||||
print "slow_log_readable: no\n";
|
||||
exit 0;
|
||||
};
|
||||
|
||||
my %fp;
|
||||
my $total = 0;
|
||||
my $max_qt = 0;
|
||||
my $max_rows_examined = 0;
|
||||
my $max_rows_sent = 0;
|
||||
|
||||
my ($active, $ts, $qt, $rows_sent, $rows_examined, $sql);
|
||||
|
||||
sub reset_entry {
|
||||
$active = 0;
|
||||
$ts = 0;
|
||||
$qt = 0;
|
||||
$rows_sent = 0;
|
||||
$rows_examined = 0;
|
||||
$sql = '';
|
||||
}
|
||||
|
||||
sub fingerprint_sql {
|
||||
my ($s) = @_;
|
||||
|
||||
$s =~ s/`[^`]*`/`X`/g;
|
||||
$s =~ s/'(?:\\.|[^'\\])*'/'X'/g;
|
||||
$s =~ s/"(?:\\.|[^"\\])*"/"X"/g;
|
||||
$s =~ s/\b[0-9a-fA-F]{16,}\b/HEX/g;
|
||||
$s =~ s/\b\d+(?:\.\d+)?\b/N/g;
|
||||
$s =~ s/\s+/ /g;
|
||||
$s =~ s/^\s+|\s+$//g;
|
||||
|
||||
return substr($s, 0, 220);
|
||||
}
|
||||
|
||||
sub flush_entry {
|
||||
return unless $active;
|
||||
return if $ts < $cutoff;
|
||||
|
||||
$total++;
|
||||
$max_qt = $qt if $qt > $max_qt;
|
||||
$max_rows_examined = $rows_examined if $rows_examined > $max_rows_examined;
|
||||
$max_rows_sent = $rows_sent if $rows_sent > $max_rows_sent;
|
||||
|
||||
my $f = fingerprint_sql($sql);
|
||||
return unless length $f;
|
||||
|
||||
$fp{$f}{count}++;
|
||||
$fp{$f}{total_time} += $qt;
|
||||
$fp{$f}{total_rows_examined} += $rows_examined;
|
||||
$fp{$f}{max_time} = $qt if !defined($fp{$f}{max_time}) || $qt > $fp{$f}{max_time};
|
||||
}
|
||||
|
||||
reset_entry();
|
||||
|
||||
while (my $line = <$fh>) {
|
||||
chomp $line;
|
||||
|
||||
if ($line =~ /^# Time:\s*(\d{2})(\d{2})(\d{2})\s+(\d{1,2}):(\d{2}):(\d{2})/) {
|
||||
flush_entry();
|
||||
reset_entry();
|
||||
|
||||
my ($yy, $mo, $dd, $hh, $mm, $ss) = ($1, $2, $3, $4, $5, $6);
|
||||
my $yyyy = $yy >= 70 ? 1900 + $yy : 2000 + $yy;
|
||||
|
||||
$ts = timelocal($ss, $mm, $hh, $dd, $mo - 1, $yyyy);
|
||||
$active = 1;
|
||||
next;
|
||||
}
|
||||
|
||||
next unless $active;
|
||||
|
||||
if ($line =~ /^# Query_time:\s*([0-9.]+).*Rows_sent:\s*([0-9]+).*Rows_examined:\s*([0-9]+)/) {
|
||||
$qt = $1 + 0;
|
||||
$rows_sent = $2 + 0;
|
||||
$rows_examined = $3 + 0;
|
||||
next;
|
||||
}
|
||||
|
||||
next if $line =~ /^# User\@Host:/;
|
||||
next if $line =~ /^use `/;
|
||||
next if $line =~ /^SET timestamp=/;
|
||||
next if $line =~ /^#/;
|
||||
next if $line =~ /^\s*$/;
|
||||
|
||||
$sql .= ' ' . $line;
|
||||
}
|
||||
|
||||
flush_entry();
|
||||
|
||||
print "slow_queries_count: $total\n";
|
||||
print "max_query_time: $max_qt\n";
|
||||
print "max_rows_examined: $max_rows_examined\n";
|
||||
print "max_rows_sent: $max_rows_sent\n";
|
||||
print "\n";
|
||||
print "top_fingerprints:\n";
|
||||
|
||||
my $shown = 0;
|
||||
for my $f (
|
||||
sort {
|
||||
$fp{$b}{total_time} <=> $fp{$a}{total_time}
|
||||
||
|
||||
$fp{$b}{total_rows_examined} <=> $fp{$a}{total_rows_examined}
|
||||
} keys %fp
|
||||
) {
|
||||
last if $shown++ >= 10;
|
||||
|
||||
printf(
|
||||
"- count=%d total_time=%.3f max_time=%.3f total_rows_examined=%d sql=%s\n",
|
||||
$fp{$f}{count},
|
||||
$fp{$f}{total_time},
|
||||
$fp{$f}{max_time},
|
||||
$fp{$f}{total_rows_examined},
|
||||
$f
|
||||
);
|
||||
}
|
||||
PERL
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Appends lsphp process count and RSS stats from an OLS pod to the diag file.
|
||||
# $1 (pod): OLS pod name.
|
||||
function diagOlsPodStats() {
|
||||
local pod="$1"
|
||||
|
||||
diagLogSection "OLS $pod lsphp count and RSS"
|
||||
|
||||
{
|
||||
echo "+ k3sRun exec pod/$pod -c openlitespeed -- sh -s <lsphp stats>"
|
||||
|
||||
k3sRun exec -i "pod/$pod" -c openlitespeed -- sh -s <<'SH' || true
|
||||
tmp="$(mktemp)"
|
||||
|
||||
echo "hostname:"
|
||||
hostname 2>/dev/null || true
|
||||
echo
|
||||
|
||||
ps -eo user=,rss=,comm=,args= > "$tmp" 2>/dev/null || ps aux > "$tmp" 2>/dev/null || true
|
||||
|
||||
echo "lsphp count:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ { c++ }
|
||||
END { print c + 0 }
|
||||
' "$tmp"
|
||||
echo
|
||||
|
||||
echo "total lsphp RSS KiB:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ { sum += $2 }
|
||||
END { print sum + 0 }
|
||||
' "$tmp"
|
||||
echo
|
||||
|
||||
echo "total lsphp RSS MiB:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ { sum += $2 }
|
||||
END { printf "%.1f\n", sum / 1024 }
|
||||
' "$tmp"
|
||||
echo
|
||||
|
||||
echo "top RSS lsphp:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ {
|
||||
print
|
||||
}
|
||||
' "$tmp" | sort -k2,2nr | head -10
|
||||
echo
|
||||
|
||||
echo "lsphp by user:"
|
||||
awk '
|
||||
/lsphp/ && $0 !~ /awk|grep/ {
|
||||
count[$1]++
|
||||
rss[$1] += $2
|
||||
}
|
||||
END {
|
||||
for (u in count) {
|
||||
printf "%s count=%d rss_kib=%d rss_mib=%.1f\n", u, count[u], rss[u], rss[u] / 1024
|
||||
}
|
||||
}
|
||||
' "$tmp" | sort -k3,3nr
|
||||
echo
|
||||
|
||||
echo "process count:"
|
||||
awk '
|
||||
NF >= 3 {
|
||||
comm=$3
|
||||
count[comm]++
|
||||
}
|
||||
END {
|
||||
for (c in count) {
|
||||
print count[c], c
|
||||
}
|
||||
}
|
||||
' "$tmp" | sort -nr | head -20
|
||||
echo
|
||||
|
||||
rm -f "$tmp"
|
||||
SH
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Parses the OLS error log since $diagSince and appends categorized error counters to the diag file.
|
||||
function diagOlsErrorSummary() {
|
||||
local openlitespeedErrorLog="$openlitespeedLogsPath/error.log"
|
||||
local cutoff
|
||||
|
||||
cutoff="$(diagCutoffEpoch)"
|
||||
|
||||
diagLogSection "OLS error counters summary"
|
||||
|
||||
{
|
||||
echo "+ summarize OLS error log: $openlitespeedErrorLog since ${diagSince:-4h}"
|
||||
echo
|
||||
|
||||
if [[ ! -r "$openlitespeedErrorLog" ]]; then
|
||||
echo "ols_error_log_readable: no"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
perl -MTime::Local - "$openlitespeedErrorLog" "$cutoff" <<'PERL'
|
||||
use strict;
|
||||
use warnings;
|
||||
use Time::Local;
|
||||
|
||||
my ($file, $cutoff) = @ARGV;
|
||||
|
||||
open my $fh, '<', $file or do {
|
||||
print "ols_error_log_readable: no\n";
|
||||
exit 0;
|
||||
};
|
||||
|
||||
my %c = (
|
||||
total_lines => 0,
|
||||
warn_count => 0,
|
||||
error_count => 0,
|
||||
hostname_mapping_conflicts => 0,
|
||||
inaccessible_vhost_root => 0,
|
||||
no_request_delivery => 0,
|
||||
connection_reset => 0,
|
||||
memory_errors => 0,
|
||||
too_many_open_files => 0,
|
||||
permission_denied => 0,
|
||||
);
|
||||
|
||||
while (my $line = <$fh>) {
|
||||
my $ts = 0;
|
||||
|
||||
if ($line =~ /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/) {
|
||||
$ts = timelocal($6, $5, $4, $3, $2 - 1, $1);
|
||||
}
|
||||
|
||||
next if $ts && $ts < $cutoff;
|
||||
|
||||
$c{total_lines}++;
|
||||
$c{warn_count}++ if $line =~ /\[WARN\]/;
|
||||
$c{error_count}++ if $line =~ /\[ERROR\]/;
|
||||
$c{hostname_mapping_conflicts}++ if $line =~ /Hostname .* is mapped to virtual host .* can.t map to virtual host/;
|
||||
$c{inaccessible_vhost_root}++ if $line =~ /Path for vhost root is not accessible/;
|
||||
$c{no_request_delivery}++ if $line =~ /No request delivery notification has been received/;
|
||||
$c{connection_reset}++ if $line =~ /Connection reset by peer/;
|
||||
$c{memory_errors}++ if $line =~ /OOM|out of memory|Cannot allocate memory/i;
|
||||
$c{too_many_open_files}++ if $line =~ /Too many open files/i;
|
||||
$c{permission_denied}++ if $line =~ /Permission denied/i;
|
||||
}
|
||||
|
||||
for my $k (
|
||||
qw(
|
||||
total_lines
|
||||
warn_count
|
||||
error_count
|
||||
hostname_mapping_conflicts
|
||||
inaccessible_vhost_root
|
||||
no_request_delivery
|
||||
connection_reset
|
||||
memory_errors
|
||||
too_many_open_files
|
||||
permission_denied
|
||||
)
|
||||
) {
|
||||
print "$k: $c{$k}\n";
|
||||
}
|
||||
PERL
|
||||
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Fetches the OPcache status endpoint four times and appends key metrics to the diag file.
|
||||
function diagOpcacheSummary() {
|
||||
local i tmp
|
||||
|
||||
diagLogSection "OPcache summary"
|
||||
|
||||
{
|
||||
echo "+ curl OPcache endpoint summary"
|
||||
echo "attempts: 4"
|
||||
echo
|
||||
|
||||
for i in 1 2 3 4; do
|
||||
tmp="$(mktemp)"
|
||||
|
||||
curl -kfsS --max-time 10 "$diagOpcacheUrl" > "$tmp" 2>/dev/null || {
|
||||
echo "attempt_$i: failed"
|
||||
rm -f "$tmp"
|
||||
continue
|
||||
}
|
||||
|
||||
echo "attempt_$i:"
|
||||
|
||||
awk '
|
||||
function val(line) {
|
||||
sub(/^.*=>[[:space:]]*/, "", line)
|
||||
if (line == "" || line == "=>") return "false"
|
||||
return line
|
||||
}
|
||||
|
||||
/\[memory_usage\]/ { ctx="memory"; next }
|
||||
/\[interned_strings_usage\]/ { ctx="interned"; next }
|
||||
/\[opcache_statistics\]/ { ctx="stats"; next }
|
||||
/\[jit\]/ { ctx="jit"; next }
|
||||
|
||||
/\[opcache_enabled\]/ { print " opcache_enabled:", val($0) }
|
||||
/\[cache_full\]/ { print " cache_full:", val($0) }
|
||||
/\[restart_pending\]/ { print " restart_pending:", val($0) }
|
||||
/\[restart_in_progress\]/ { print " restart_in_progress:", val($0) }
|
||||
|
||||
ctx=="memory" && /\[used_memory\]/ { print " memory_used:", val($0) }
|
||||
ctx=="memory" && /\[free_memory\]/ { print " memory_free:", val($0) }
|
||||
ctx=="memory" && /\[wasted_memory\]/ { print " memory_wasted:", val($0) }
|
||||
ctx=="memory" && /\[current_wasted_percentage\]/ { print " memory_wasted_pct:", val($0) }
|
||||
|
||||
ctx=="interned" && /\[buffer_size\]/ { print " interned_buffer_size:", val($0) }
|
||||
ctx=="interned" && /\[used_memory\]/ { print " interned_used:", val($0) }
|
||||
ctx=="interned" && /\[free_memory\]/ { print " interned_free:", val($0) }
|
||||
ctx=="interned" && /\[number_of_strings\]/ { print " interned_strings:", val($0) }
|
||||
|
||||
ctx=="stats" && /\[num_cached_scripts\]/ { print " num_cached_scripts:", val($0) }
|
||||
ctx=="stats" && /\[num_cached_keys\]/ { print " num_cached_keys:", val($0) }
|
||||
ctx=="stats" && /\[max_cached_keys\]/ { print " max_cached_keys:", val($0) }
|
||||
ctx=="stats" && /\[hits\]/ { print " hits:", val($0) }
|
||||
ctx=="stats" && /\[misses\]/ { print " misses:", val($0) }
|
||||
ctx=="stats" && /\[oom_restarts\]/ { print " oom_restarts:", val($0) }
|
||||
ctx=="stats" && /\[hash_restarts\]/ { print " hash_restarts:", val($0) }
|
||||
ctx=="stats" && /\[manual_restarts\]/ { print " manual_restarts:", val($0) }
|
||||
ctx=="stats" && /\[opcache_hit_rate\]/ { print " opcache_hit_rate:", val($0) }
|
||||
' "$tmp" || true
|
||||
|
||||
rm -f "$tmp"
|
||||
echo
|
||||
done
|
||||
} >> "$diagFile" 2>&1
|
||||
}
|
||||
|
||||
# Collects system-level diagnostics (uptime, memory, vmstat, iostat, PSI, sar, disk usage).
|
||||
function diagSystem() {
|
||||
diagLogSection "uptime"
|
||||
diagCmd uptime
|
||||
|
||||
diagLogSection "free -m"
|
||||
diagCmd free -m
|
||||
|
||||
diagLogSection "vmstat 1 10"
|
||||
diagCmd vmstat 1 10
|
||||
|
||||
diagLogSection "mpstat 1 10"
|
||||
diagCmd mpstat 1 10
|
||||
|
||||
diagLogSection "iostat -x -z 1 5 filtered"
|
||||
diagIostat
|
||||
|
||||
diagLogSection "cat /proc/pressure/cpu"
|
||||
diagCmd cat /proc/pressure/cpu
|
||||
|
||||
diagLogSection "cat /proc/pressure/memory"
|
||||
diagCmd cat /proc/pressure/memory
|
||||
|
||||
diagLogSection "cat /proc/pressure/io"
|
||||
diagCmd cat /proc/pressure/io
|
||||
|
||||
diagSarTry "sar queue last ${diagSince:-4h}" "sar -q -s '$(diagSarStartTime)'"
|
||||
diagSarTry "sar cpu last ${diagSince:-4h}" "sar -u -s '$(diagSarStartTime)'"
|
||||
diagSarTry "sar memory last ${diagSince:-4h}" "sar -r -s '$(diagSarStartTime)'"
|
||||
diagSarTry "sar swap last ${diagSince:-4h}" "sar -W -s '$(diagSarStartTime)'"
|
||||
|
||||
diagLogSection "Filesystem pressure quick checks"
|
||||
diagDfUsage
|
||||
|
||||
diagLogSection "Filesystem inode quick checks"
|
||||
diagDfInodes
|
||||
}
|
||||
|
||||
# Collects k3s diagnostics (pod top/list, warning events, restart summary, node conditions).
|
||||
function diagK3s() {
|
||||
diagLogSection "Top pod"
|
||||
diagK3sTry top pod
|
||||
|
||||
diagLogSection "Get pod"
|
||||
diagK3sTry get pod
|
||||
|
||||
diagLogSection "Get warning events recent"
|
||||
diagK3sTry get events --field-selector type!=Normal --sort-by=.lastTimestamp
|
||||
|
||||
diagLogSection "Pod restart summary"
|
||||
diagK3sTry get pod -o custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount,STATE:.status.containerStatuses[*].state.waiting.reason,LAST_STATE:.status.containerStatuses[*].lastState.terminated.reason
|
||||
|
||||
diagLogSection "Node conditions and allocated resources"
|
||||
diagK3sNodeSummary
|
||||
}
|
||||
|
||||
# Collects MariaDB diagnostics (master/slave global status, replication summary, slow queries).
|
||||
function diagMariaDB() {
|
||||
diagLogSection "MariaDB master global status"
|
||||
{
|
||||
echo "+ mariadbMasterRootQuery"
|
||||
mariadbMasterRootQuery "
|
||||
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
||||
'Uptime',
|
||||
'Threads_running',
|
||||
'Threads_connected',
|
||||
'Max_used_connections',
|
||||
'Connections',
|
||||
'Aborted_connects',
|
||||
'Slow_queries',
|
||||
'Questions',
|
||||
'Queries',
|
||||
'Created_tmp_tables',
|
||||
'Created_tmp_disk_tables',
|
||||
'Innodb_buffer_pool_reads',
|
||||
'Innodb_buffer_pool_read_requests',
|
||||
'Innodb_data_reads',
|
||||
'Innodb_data_writes',
|
||||
'Innodb_log_waits',
|
||||
'Open_tables',
|
||||
'Opened_tables',
|
||||
'Table_open_cache_hits',
|
||||
'Table_open_cache_misses',
|
||||
'Table_open_cache_overflows'
|
||||
);
|
||||
" || true
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
|
||||
diagLogSection "MariaDB slave global status"
|
||||
{
|
||||
echo "+ mariadbSlaveRootQuery"
|
||||
mariadbSlaveRootQuery "
|
||||
SHOW GLOBAL STATUS WHERE Variable_name IN (
|
||||
'Uptime',
|
||||
'Threads_running',
|
||||
'Threads_connected',
|
||||
'Max_used_connections',
|
||||
'Connections',
|
||||
'Aborted_connects',
|
||||
'Slow_queries',
|
||||
'Questions',
|
||||
'Queries',
|
||||
'Created_tmp_tables',
|
||||
'Created_tmp_disk_tables',
|
||||
'Innodb_buffer_pool_reads',
|
||||
'Innodb_buffer_pool_read_requests',
|
||||
'Innodb_data_reads',
|
||||
'Innodb_data_writes',
|
||||
'Innodb_log_waits',
|
||||
'Slave_running',
|
||||
'Slave_received_heartbeats',
|
||||
'Slave_heartbeat_period',
|
||||
'Slave_open_temp_tables'
|
||||
);
|
||||
" || true
|
||||
echo
|
||||
} >> "$diagFile" 2>&1
|
||||
|
||||
diagMariaDBReplicaSummary
|
||||
diagMariaDBSlowSummary
|
||||
}
|
||||
|
||||
# Collects OpenLiteSpeed diagnostics (pod list, per-pod lsphp stats, error summary, OPcache).
|
||||
function diagOpenLiteSpeed() {
|
||||
local podList pod
|
||||
|
||||
diagLogSection "OpenLiteSpeed pods"
|
||||
diagK3sTry get pod -l app=openlitespeed
|
||||
|
||||
podList="$(k3sPodListApp openlitespeed 2>/dev/null || true)"
|
||||
while read -r pod; do
|
||||
[[ -z "$pod" ]] && continue
|
||||
diagOlsPodStats "$pod"
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
|
||||
diagOlsErrorSummary
|
||||
diagOpcacheSummary
|
||||
}
|
||||
|
||||
# Generates a full diagnostic report and writes it to $diagFile.
|
||||
# [$1] (diagFile): output file path (defaults to $diagFile).
|
||||
function diagReport() {
|
||||
local diagFile="${1:-$diagFile}"
|
||||
local aiModel="${1:-short}"
|
||||
|
||||
export LC_ALL=C
|
||||
export LANG=C
|
||||
|
||||
local reportPath
|
||||
reportPath=$(dirname "$diagFile")
|
||||
[[ -d "$reportPath" ]] || mkdir -p "$reportPath"
|
||||
|
||||
if [[ "$postfixHost" ]]; then
|
||||
diagExcludeVhostsRegex="$diagExcludeVhostsRegex|${postfixHost//./\\.}"
|
||||
fi
|
||||
|
||||
: > "$diagFile"
|
||||
diagMeta
|
||||
diagSystem
|
||||
diagK3s
|
||||
diagMariaDB
|
||||
diagOpenLiteSpeed
|
||||
diagSanitizeReport
|
||||
}
|
||||
|
||||
# Uploads the diag report file to the remote API.
|
||||
# [$1] (diagFile): report file path (defaults to $diagFile).
|
||||
# [$2] (aiModelId): AI endpoint ID to use (defaults to 1).
|
||||
function diagSend() {
|
||||
local diagFile="${1:-$diagFile}"
|
||||
local aiModelId="${2:-1}"
|
||||
|
||||
logInfo "$diagLabel Sending data..."
|
||||
curl -fsS -X POST "$diagApiUrl" -F "source_type=worker" -F "source_id=$hostUuid" -F "generated_time=$(date +%s)" -F "report_file=@$diagFile" -F "ai_endpoint_id=$aiModelId"
|
||||
logSuccess "$diagLabel Data sent successfully"
|
||||
}
|
||||
|
||||
# Generates the full diagnostic report and sends it to the remote API.
|
||||
# [$1] (aiModel): model name: full or short (defaults to short).
|
||||
function diagRun() {
|
||||
local aiModel="${1:-short}"
|
||||
local aiModelId=2
|
||||
|
||||
case "$aiModel" in
|
||||
full)
|
||||
aiModelId=1
|
||||
;;
|
||||
*)
|
||||
aiModelId=2
|
||||
;;
|
||||
esac
|
||||
|
||||
diagReport "$diagFile"
|
||||
diagSend "$diagFile" "$aiModelId"
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# Waits for the k3s API to become ready by polling /readyz.
|
||||
# Retries up to $k3sReadyRetries times with $k3sReadySleep seconds between attempts.
|
||||
function k3sReady() {
|
||||
local tries=${k3sReadyRetries:-10}
|
||||
local delay=${k3sReadySleep:-2}
|
||||
local i
|
||||
for ((i=1; i<=tries; i++)); do
|
||||
"$k3sCmd" kubectl get --raw=/readyz >/dev/null 2>&1 && return 0
|
||||
sleep "$delay"
|
||||
done
|
||||
|
||||
appError "k3s API not ready after $tries tries"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Validates a YAML file against the Kubernetes API (dry-run).
|
||||
# $1 (file): path to the YAML file.
|
||||
function k3sYamlCheck() {
|
||||
local file="$1"
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
[[ -f "$file" ]] || { appError "File not found: $file"; return 1; }
|
||||
|
||||
k3sReady
|
||||
runFail "$k3sCmd" kubectl apply --dry-run=client -f "$file"
|
||||
}
|
||||
|
||||
# Applies a YAML configuration to Kubernetes.
|
||||
# $1 (file): path to the YAML configuration file.
|
||||
function k3sYamlApply() {
|
||||
local file="$1" output error
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
|
||||
run output error "$k3sCmd" kubectl apply -f "$file" --validate=false --request-timeout=60s || {
|
||||
appError "Error applying YAML: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Runs kubectl in $k3sNamespace after ensuring k3s is ready.
|
||||
function k3sRun() {
|
||||
k3sReady || return 1
|
||||
"$k3sCmd" kubectl -n "$k3sNamespace" "$@"
|
||||
}
|
||||
|
||||
# Lists pod names sorted alphabetically, optionally filtered by app label.
|
||||
# [$1] (app): app label value to filter by (optional).
|
||||
function k3sPodList() {
|
||||
local args=()
|
||||
[[ -n "${1-}" ]] && args+=(-l "app=$1")
|
||||
|
||||
k3sRun get pods "${args[@]}" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' --sort-by='{.metadata.name}'
|
||||
}
|
||||
|
||||
# Lists pod names with their phase (name|phase), optionally filtered by app label.
|
||||
# [$1] (app): app label value to filter by (optional).
|
||||
function k3sPodListStatus() {
|
||||
local args=()
|
||||
[[ -n "${1-}" ]] && args+=(-l "app=$1")
|
||||
|
||||
local raw
|
||||
raw=$(k3sRun get pods "${args[@]}" \
|
||||
-o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.status.phase}{"|"}{range .status.conditions[*]}{.type}{"="}{.status}{" "}{end}{"\n"}{end}' \
|
||||
--sort-by='{.metadata.name}') || return 1
|
||||
|
||||
awk -F'|' 'NF {
|
||||
if ($2 == "Running" && $3 ~ /(^| )DisruptionTarget=True( |$)/)
|
||||
status = "Terminating"
|
||||
else if ($2 == "Running" && $3 !~ /(^| )Ready=True( |$)/)
|
||||
status = "NotReady"
|
||||
else
|
||||
status = $2
|
||||
print $1 "|" status
|
||||
}' <<< "$raw"
|
||||
}
|
||||
|
||||
# Captures the current list of pod names into an array variable.
|
||||
# $1 (varname): name of the array variable to populate.
|
||||
function k3sPodsSnapshot() {
|
||||
local -n __out="$1"
|
||||
mapfile -t __out < <(
|
||||
k3sRun get pods --no-headers 2>/dev/null | awk '{print $1}'
|
||||
)
|
||||
}
|
||||
|
||||
# Waits until all pods not in the baseline snapshot are Running/Succeeded/Completed.
|
||||
# $1 (varname): name of the baseline array variable (from k3sPodsSnapshot).
|
||||
# [$2] (timeout): max wait in seconds (default 240).
|
||||
# [$3] (interval): poll interval in seconds (default 4).
|
||||
function k3sWaitNewPodsReady() {
|
||||
local -n baseline="$1"
|
||||
local timeout="${2:-240}"
|
||||
local interval="${3:-4}"
|
||||
|
||||
local attempts=$(( timeout / interval ))
|
||||
(( attempts < 1 )) && attempts=1
|
||||
|
||||
local i notReady newSeen
|
||||
for ((i=1; i<=attempts; i++)); do
|
||||
mapfile -t _now < <(
|
||||
k3sRun get pods --no-headers 2>/dev/null | awk '{print $1, $3}'
|
||||
)
|
||||
|
||||
notReady=0
|
||||
newSeen=0
|
||||
local ln name status
|
||||
for ln in "${_now[@]}"; do
|
||||
[[ -z "$ln" ]] && continue
|
||||
name="${ln%% *}"
|
||||
status="${ln#* }"
|
||||
|
||||
if ! arrayContains "$name" "${baseline[@]}"; then
|
||||
newSeen=1
|
||||
if [[ "$status" != "Running" && "$status" != "Succeeded" && "$status" != "Completed" ]]; then
|
||||
((notReady++))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $newSeen -eq 0 || $notReady -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $i -lt $attempts ]]; then
|
||||
printDotText "pods not ready: $fontBlue$notReady$fontReset" "${fontYellow}waiting$fontReset"
|
||||
sleep "$interval"
|
||||
else
|
||||
printDotText "pods not ready: $fontBlue$notReady$fontReset" "${fontRed}waiting time's up$fontReset"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
# Executes a command inside the MariaDB master pod.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function mariadbMasterExec() {
|
||||
k3sRun exec pod/"$mariadbMasterKube"-0 -c "$mariadbMasterKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside the MariaDB master pod with stdin attached (-i).
|
||||
# $@ (...): command and arguments to execute.
|
||||
function mariadbMasterExecI() {
|
||||
k3sRun exec -i pod/"$mariadbMasterKube"-0 -c "$mariadbMasterKube" -- "$@"
|
||||
}
|
||||
|
||||
# Executes a command inside the MariaDB slave pod.
|
||||
# $@ (...): command and arguments to execute.
|
||||
function mariadbSlaveExec() {
|
||||
k3sRun exec pod/"$mariadbSlaveKube"-0 -c "$mariadbSlaveKube" -- "$@"
|
||||
}
|
||||
|
||||
# Runs a SQL query as root against the specified MariaDB pod.
|
||||
# $1 (target): master|slave
|
||||
# $2 (query): SQL query.
|
||||
# [$3] (showColumn): pass "showColumn" to keep column headers.
|
||||
function mariadbRootQuery() {
|
||||
local target="$1" query="$2"
|
||||
[[ -n "$query" ]] || { appError "Query not specified"; return 1; }
|
||||
|
||||
local execFn
|
||||
case "$target" in
|
||||
master) execFn=mariadbMasterExec ;;
|
||||
slave) execFn=mariadbSlaveExec ;;
|
||||
*) appError "Unknown target: $target"; return 1 ;;
|
||||
esac
|
||||
|
||||
if [[ "${3-}" == "showColumn" ]]; then
|
||||
"$execFn" mariadb -uroot -p"$mariadbRootPass" -e "$query"
|
||||
else
|
||||
"$execFn" mariadb -uroot -p"$mariadbRootPass" -N -e "$query"
|
||||
fi
|
||||
}
|
||||
|
||||
# Runs a SQL query as root against the MariaDB master pod.
|
||||
# $1 (query): SQL query.
|
||||
# [$2] (showColumn): pass "showColumn" to keep column headers.
|
||||
function mariadbMasterRootQuery() { mariadbRootQuery master "$@"; }
|
||||
# Runs a SQL query as root against the MariaDB slave pod.
|
||||
# $1 (query): SQL query.
|
||||
# [$2] (showColumn): pass "showColumn" to keep column headers.
|
||||
function mariadbSlaveRootQuery() { mariadbRootQuery slave "$@"; }
|
||||
|
||||
# Converts a domain name into a MariaDB-safe identifier (max 60 chars).
|
||||
# $1 (domain): domain name.
|
||||
function mariadbDomain2id() {
|
||||
domainToRandom "$1" 60
|
||||
}
|
||||
|
||||
# Reads the MariaDB root password from the Kubernetes secret.
|
||||
function mariadbRootPassSecretGet() {
|
||||
k3sRun get secret "$mariadbKube-secret" -o jsonpath="{.data.root-password}" --ignore-not-found | base64 -d
|
||||
}
|
||||
|
||||
# Saves the MariaDB root password from the Kubernetes secret to a local file.
|
||||
function mariadbRootPassSecretSave() {
|
||||
local password
|
||||
password="$(mariadbRootPassSecretGet)"
|
||||
[[ -n "$password" ]] && printf '%s\n' "$password" > "$appDataPath/$hostName.$mariadbKube"
|
||||
}
|
||||
|
||||
# Lists user-created MariaDB databases (system schemas excluded).
|
||||
function mariadbDatabaseListGet() {
|
||||
local output error
|
||||
run output error mariadbMasterRootQuery "SHOW DATABASES;" || { appError "$error"; return 1; }
|
||||
printf '%s\n' "$output" | awk '!/^(information_schema|performance_schema|mysql|sys)$/'
|
||||
}
|
||||
|
||||
# Lists MariaDB users with Host=% (root and replication_user excluded).
|
||||
function mariadbUserListGet() {
|
||||
local output error
|
||||
run output error mariadbMasterRootQuery "SELECT user FROM mysql.user WHERE Host = '%';" || { appError "$error"; return 1; }
|
||||
printf '%s\n' "$output" | awk '!/^(root|replication_user)$/'
|
||||
}
|
||||
|
||||
# Creates or updates a MariaDB database and user with full privileges.
|
||||
# Revokes privileges from any other users previously assigned to the same database.
|
||||
# $1 (database): database name.
|
||||
# $2 (username): database username.
|
||||
# $3 (password): database user password.
|
||||
function mariadbDatabaseUserSet() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
local username="$2"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$3"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
mariadbMasterRootQuery "CREATE DATABASE IF NOT EXISTS \`$database\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER IF NOT EXISTS '$username'@'%' IDENTIFIED BY '$password'; ALTER USER '$username'@'%' IDENTIFIED BY '$password'; GRANT ALL PRIVILEGES ON \`$database\`.* TO '$username'@'%' WITH MAX_USER_CONNECTIONS 15;" || {
|
||||
appError "Create/update database user failed: database=$database username=$username"
|
||||
return 1
|
||||
}
|
||||
|
||||
local others
|
||||
others="$(mariadbMasterRootQuery "SELECT DISTINCT User, Host FROM mysql.db WHERE Db='$database' AND NOT (User='$username' AND Host='%');")"
|
||||
while IFS=$'\t' read -r u h; do
|
||||
[[ -z "$u" || -z "$h" ]] && continue
|
||||
mariadbMasterRootQuery "REVOKE ALL PRIVILEGES ON \`$database\`.* FROM '$u'@'$h';" || \
|
||||
appError "Revoke privileges failed: database=$database user=$u host=$h"
|
||||
done <<< "$others"
|
||||
}
|
||||
|
||||
# Checks whether a MariaDB database exists.
|
||||
# $1 (database): database name.
|
||||
function mariadbDatabaseExists() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
|
||||
local out
|
||||
out="$(mariadbMasterRootQuery "SHOW DATABASES LIKE '$database';" 2>/dev/null | tail -n 1 | tr -d '\r')"
|
||||
[[ "$out" == "$database" ]]
|
||||
}
|
||||
|
||||
# Removes a MariaDB database if it exists.
|
||||
# $1 (database): database name.
|
||||
function mariadbDatabaseRemove() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
mariadbMasterRootQuery "DROP DATABASE IF EXISTS \`$database\`;"
|
||||
}
|
||||
|
||||
# Removes a MariaDB user from all hosts.
|
||||
# $1 (username): MariaDB username.
|
||||
function mariadbUserRemove() {
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
|
||||
local hosts
|
||||
hosts="$(mariadbMasterRootQuery "SELECT Host FROM mysql.user WHERE User='$username';")"
|
||||
while read -r host; do
|
||||
mariadbMasterRootQuery "DROP USER IF EXISTS '$username'@'$host';"
|
||||
done <<< "$hosts"
|
||||
}
|
||||
|
||||
# Removes all tables from a MariaDB database.
|
||||
# $1 (database): database name.
|
||||
# $2 (username): database username.
|
||||
# $3 (password): database user password.
|
||||
function mariadbDatabaseClean() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
local username="$2"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$3"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
|
||||
local output error
|
||||
run output error mariadbMasterExec mariadb -u "$username" -p"$password" -N -e "SELECT CONCAT('DROP TABLE \`', table_name, '\`;') FROM information_schema.tables WHERE table_schema = '$database';" || { appError "$error"; return 1; }
|
||||
run output error mariadbMasterExec mariadb -u "$username" -p"$password" -e "USE \`$database\`; SET FOREIGN_KEY_CHECKS = 0; $output SET FOREIGN_KEY_CHECKS = 1;" || { appError "$error"; return 1; }
|
||||
}
|
||||
|
||||
# Base: runs mariadb-dump via the given exec function.
|
||||
# $1 (execFn): mariadbMasterExec|mariadbSlaveExec.
|
||||
# $2 (username), $3 (password), $4 (database|all).
|
||||
# [$5] (file): auto-generated if omitted.
|
||||
# [$6+] (...): optional mariadb-dump parameters.
|
||||
function mariadbExport() {
|
||||
local execFn="$1"
|
||||
[[ -n "$execFn" ]] || { appError "Exec function not specified"; return 1; }
|
||||
shift
|
||||
|
||||
local username="$1"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
shift
|
||||
|
||||
local password="$1"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
shift
|
||||
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
shift
|
||||
|
||||
local file="$1"
|
||||
if [[ -z "$file" ]]; then
|
||||
[[ "$database" == "all" ]] \
|
||||
&& file="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_full.sql.tar.gz" \
|
||||
|| file="$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_$database.sql.tar.gz"
|
||||
fi
|
||||
shift
|
||||
|
||||
local -a params
|
||||
if [[ $# -gt 0 ]]; then
|
||||
params=("$@")
|
||||
local haveAll=false haveDatabases=false havePositional=false
|
||||
for a in "${params[@]}"; do
|
||||
case "$a" in
|
||||
--all-databases|-A) haveAll=true ;;
|
||||
--databases|-B) haveDatabases=true ;;
|
||||
-*) ;;
|
||||
*) havePositional=true ;;
|
||||
esac
|
||||
done
|
||||
if ! $haveAll && ! $haveDatabases && ! $havePositional; then
|
||||
[[ "$database" == "all" ]] && params+=(--all-databases) || params+=("$database")
|
||||
fi
|
||||
else
|
||||
if [[ "$database" == "all" ]]; then
|
||||
params=(--all-databases --single-transaction --quick --master-data=2 --routines --events)
|
||||
else
|
||||
params=("$database" --single-transaction --quick --routines --events)
|
||||
fi
|
||||
fi
|
||||
|
||||
local filePath fileBase fileSql
|
||||
filePath=$(dirname -- "$file")
|
||||
fileBase=$(basename -- "$file")
|
||||
case "$fileBase" in
|
||||
*.zip) fileSql="${fileBase%.zip}" ;;
|
||||
*.tar.gz) fileSql="${fileBase%.tar.gz}" ;;
|
||||
*) fileSql="$fileBase" ;;
|
||||
esac
|
||||
|
||||
mkdir -p -- "$filePath" || { appError "Failed to create directory: $filePath"; return 1; }
|
||||
rm -f -- "$filePath/$fileSql" "$filePath/$fileBase" &>/dev/null
|
||||
|
||||
local output error
|
||||
runShell output error "$execFn" mariadb-dump -u "$username" -p"$password" "${params[@]}" ">" "$filePath/$fileSql" || {
|
||||
rm -f -- "$filePath/$fileSql" &>/dev/null
|
||||
appError "Error creating dump: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$fileBase" in
|
||||
*.zip|*.tar.gz|*.tgz)
|
||||
runError error archiveCreate "$filePath/$fileSql" "$file" || {
|
||||
rm -f -- "$filePath/$fileSql" "$filePath/$fileBase" &>/dev/null
|
||||
appError "Error creating archive dump: $error"
|
||||
return 1
|
||||
}
|
||||
rm -f -- "$filePath/$fileSql" &>/dev/null
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s' "$file"
|
||||
}
|
||||
|
||||
# Exports from the master pod.
|
||||
# $1 (username), $2 (password), $3 (database|all), [$4] (file), [$5+] (params)
|
||||
function mariadbMasterExport() {
|
||||
mariadbExport mariadbMasterExec "$@"
|
||||
}
|
||||
|
||||
# Exports using root credentials.
|
||||
# $1 (execFn), $2 (database|all), [$3] (file), [$4+] (params)
|
||||
function mariadbRootExport() {
|
||||
mariadbExport "$1" root "$mariadbRootPass" "${@:2}"
|
||||
}
|
||||
|
||||
# Exports from master using root credentials.
|
||||
# [$1] (database|all), [$2] (file), [$3+] (params)
|
||||
function mariadbMasterRootExport() {
|
||||
local target="${1:-all}"
|
||||
local file="${2:-$appDataPath/$mariadbMasterKube/${appDate}_${appTime}_full.sql.tar.gz}"
|
||||
mariadbRootExport mariadbMasterExec "$target" "$file" ${@:3}
|
||||
}
|
||||
|
||||
# Exports from slave using root credentials.
|
||||
# [$1] (database|all), [$2] (file)
|
||||
function mariadbSlaveRootExport() {
|
||||
local target="${1:-all}"
|
||||
local file="${2:-$appDataPath/$mariadbSlaveKube/${appDate}_${appTime}_full.sql.tar.gz}"
|
||||
mariadbRootExport mariadbSlaveExec "$target" "$file" --all-databases --single-transaction --quick --routines --events
|
||||
}
|
||||
|
||||
# Imports a MariaDB dump into a database.
|
||||
# Supports plain SQL, .zip and .tar.gz archives (must contain exactly one file).
|
||||
# $1 (database|all): target database, or "all" to import without selecting a database.
|
||||
# $2 (username): username used for import.
|
||||
# $3 (password): password used for import.
|
||||
# $4 (file): source dump file.
|
||||
function mariadbMasterImport() {
|
||||
local database="$1"
|
||||
[[ -n "$database" ]] || { appError "Database not specified"; return 1; }
|
||||
local username="$2"
|
||||
[[ -n "$username" ]] || { appError "Username not specified"; return 1; }
|
||||
local password="$3"
|
||||
[[ -n "$password" ]] || { appError "Password not specified"; return 1; }
|
||||
local file="$4"
|
||||
[[ -n "$file" ]] || { appError "File not specified"; return 1; }
|
||||
[[ -f "$file" ]] || { appError "File not found: $file"; return 1; }
|
||||
|
||||
local tmpFile
|
||||
if [[ "$file" == *.zip ]]; then
|
||||
local entriesRaw
|
||||
entriesRaw="$(unzip -Z1 "$file" 2>/dev/null)" || { appError "Not a valid zip archive: $file"; return 1; }
|
||||
|
||||
local -a entries=()
|
||||
mapfile -t entries < <(printf '%s\n' "$entriesRaw" | sed '/\/$/d')
|
||||
[[ ${#entries[@]} -eq 1 ]] || { appError "Archive must contain exactly one file: $file"; return 1; }
|
||||
|
||||
tmpFile="$(mktemp)" || { appError "Failed to create temporary file"; return 1; }
|
||||
unzip -p "$file" "${entries[0]}" > "$tmpFile" 2>/dev/null || {
|
||||
rm -f -- "$tmpFile"
|
||||
appError "Failed to extract ZIP archive"
|
||||
return 1
|
||||
}
|
||||
elif [[ "$file" == *.tar.gz ]]; then
|
||||
local entriesRaw
|
||||
entriesRaw="$(tar -tzf "$file" 2>/dev/null)" || { appError "Not a valid tar.gz archive: $file"; return 1; }
|
||||
|
||||
local -a entries=()
|
||||
mapfile -t entries < <(printf '%s\n' "$entriesRaw" | sed '/\/$/d')
|
||||
[[ ${#entries[@]} -eq 1 ]] || { appError "Archive must contain exactly one file: $file"; return 1; }
|
||||
|
||||
tmpFile="$(mktemp)" || { appError "Failed to create temporary file"; return 1; }
|
||||
tar -xOzf "$file" "${entries[0]}" > "$tmpFile" 2>/dev/null || {
|
||||
rm -f -- "$tmpFile"
|
||||
appError "Failed to extract tar.gz archive"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
tmpFile="$file"
|
||||
fi
|
||||
|
||||
if [[ ! -s "$tmpFile" ]]; then
|
||||
[[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile"
|
||||
appError "The SQL dump is empty"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a mariadbCmd=(mariadbMasterExecI mariadb -u "$username" -p"$password")
|
||||
[[ "$database" != "all" ]] && mariadbCmd+=("$database")
|
||||
|
||||
local output error
|
||||
runShell output error "${mariadbCmd[@]}" "<" "$tmpFile" ">" /dev/null || {
|
||||
[[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile"
|
||||
appError "Import failed: ${error:-$output}"
|
||||
return 1
|
||||
}
|
||||
|
||||
[[ "$tmpFile" == "$file" ]] || rm -f -- "$tmpFile"
|
||||
}
|
||||
|
||||
# Imports a MariaDB dump using root credentials.
|
||||
# $1 (database|all): target database, or "all".
|
||||
# $2 (file): source dump file.
|
||||
function mariadbMasterRootImport() {
|
||||
mariadbMasterImport "$1" root "$mariadbRootPass" "${@:2}"
|
||||
}
|
||||
|
||||
# Creates or updates the MariaDB database and user for a site.
|
||||
# $1 (domain): site domain name.
|
||||
function mariadbConfigRebuild() {
|
||||
local domain
|
||||
domain=$(domainPrepare "$1")
|
||||
domainCheck "$domain" || return 1
|
||||
|
||||
local databaseName databaseUser databasePass
|
||||
databaseName=$(siteConfigGetOrSet "$domain" "databaseName" "$(mariadbDomain2id "$domain")")
|
||||
databaseUser=$(siteConfigGetOrSet "$domain" "databaseUser" "$(mariadbDomain2id "$domain")")
|
||||
databasePass=$(siteConfigGetOrCreate "$domain" "databasePass")
|
||||
mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass" || return 1
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
# Restarts the vmagent deployment; errors are intentionally ignored.
|
||||
function metricRestart() {
|
||||
k3sRun rollout restart deployment/"$metricKube"-vmagent || true
|
||||
}
|
||||
|
||||
# Writes Prometheus metrics (build info, vhost counts, domain counts, pod memory) to $metricPromPath/kube.prom.
|
||||
function metricKube() {
|
||||
local fileProm="$metricPromPath/kube.prom"
|
||||
local fileTmp="${fileProm}.tmp"
|
||||
|
||||
mkdir -p -- "$metricPromPath" || return 1
|
||||
|
||||
local vhostAllCount vhostUpCount
|
||||
vhostAllCount=$(openlitespeedVhostList all | grep -c . || true)
|
||||
vhostUpCount=$(openlitespeedVhostList up | grep -c . || true)
|
||||
|
||||
local appTimestamp
|
||||
appTimestamp=$(date -d "$appDate ${appTime//-/:}" +%s)
|
||||
|
||||
local domainAllCount=-1 domainFreeCount=-1
|
||||
[[ -f "$domainsList" ]] && domainAllCount=$(grep -cP '^(?!\s*$)' "$domainsList" || true)
|
||||
[[ -f "$domainsList" ]] && domainFreeCount=$(grep -c '^#' "$domainsList" || true)
|
||||
|
||||
local masterMemReq=-1 masterMemLim=-1
|
||||
local slaveMemReq=-1 slaveMemLim=-1
|
||||
local olsMemReq=-1 olsMemLim=-1
|
||||
local redisMemReq=-1 redisMemLim=-1
|
||||
local _line _req _lim
|
||||
|
||||
_line=$(k3sRun get pod "${mariadbMasterKube}-0" \
|
||||
-o jsonpath="{.spec.containers[?(@.name=='${mariadbMasterKube}')].resources.requests.memory} {.spec.containers[?(@.name=='${mariadbMasterKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
masterMemReq=$(sizeToBytes "$_req"); masterMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
_line=$(k3sRun get pod "${mariadbSlaveKube}-0" \
|
||||
-o jsonpath="{.spec.containers[?(@.name=='${mariadbSlaveKube}')].resources.requests.memory} {.spec.containers[?(@.name=='${mariadbSlaveKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
slaveMemReq=$(sizeToBytes "$_req"); slaveMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
_line=$(k3sRun get pods -l "app=$openlitespeedKube" \
|
||||
-o jsonpath="{.items[0].spec.containers[?(@.name=='${openlitespeedKube}')].resources.requests.memory} {.items[0].spec.containers[?(@.name=='${openlitespeedKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
olsMemReq=$(sizeToBytes "$_req"); olsMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
_line=$(k3sRun get pod "${redisKube}-0" \
|
||||
-o jsonpath="{.spec.containers[?(@.name=='${redisKube}')].resources.requests.memory} {.spec.containers[?(@.name=='${redisKube}')].resources.limits.memory}" 2>/dev/null)
|
||||
read -r _req _lim <<< "$_line"
|
||||
redisMemReq=$(sizeToBytes "$_req"); redisMemLim=$(sizeToBytes "$_lim")
|
||||
|
||||
cat > "$fileTmp" <<EOF
|
||||
# HELP kube_build_info Build and version info
|
||||
# TYPE kube_build_info gauge
|
||||
kube_build_info{version="${appVersion}"} 1
|
||||
|
||||
# HELP kube_start_time Time snapshot
|
||||
# TYPE kube_start_time gauge
|
||||
kube_start_time $appTimestamp
|
||||
|
||||
# HELP kube_vhost_all_count Number of virtual hosts
|
||||
# TYPE kube_vhost_all_count gauge
|
||||
kube_vhost_all_count $vhostAllCount
|
||||
|
||||
# HELP kube_vhost_up_count Number of active virtual hosts
|
||||
# TYPE kube_vhost_up_count gauge
|
||||
kube_vhost_up_count $vhostUpCount
|
||||
|
||||
# HELP kube_domain_all_count Number of domains
|
||||
# TYPE kube_domain_all_count gauge
|
||||
kube_domain_all_count $domainAllCount
|
||||
|
||||
# HELP kube_domain_use_count Number of use domains
|
||||
# TYPE kube_domain_use_count gauge
|
||||
kube_domain_use_count $domainFreeCount
|
||||
|
||||
# HELP kube_pod_memory_request_bytes Pod memory request in bytes
|
||||
# TYPE kube_pod_memory_request_bytes gauge
|
||||
kube_pod_memory_request_bytes{component="mariadb-master"} $masterMemReq
|
||||
kube_pod_memory_request_bytes{component="mariadb-slave"} $slaveMemReq
|
||||
kube_pod_memory_request_bytes{component="openlitespeed"} $olsMemReq
|
||||
kube_pod_memory_request_bytes{component="redis"} $redisMemReq
|
||||
|
||||
# HELP kube_pod_memory_limit_bytes Pod memory limit in bytes
|
||||
# TYPE kube_pod_memory_limit_bytes gauge
|
||||
kube_pod_memory_limit_bytes{component="mariadb-master"} $masterMemLim
|
||||
kube_pod_memory_limit_bytes{component="mariadb-slave"} $slaveMemLim
|
||||
kube_pod_memory_limit_bytes{component="openlitespeed"} $olsMemLim
|
||||
kube_pod_memory_limit_bytes{component="redis"} $redisMemLim
|
||||
EOF
|
||||
mv "$fileTmp" "$fileProm"
|
||||
}
|
||||
|
||||
# Collects lsphp CPU/memory/worker metrics per domain across OLS pods and writes to $metricPromPath/lsphp.prom.
|
||||
function metricLsphp() {
|
||||
local fileProm="$metricPromPath/lsphp.prom"
|
||||
local fileTmp="${fileProm}.tmp"
|
||||
|
||||
mkdir -p -- "$metricPromPath" || return 1
|
||||
|
||||
local podList error
|
||||
run podList error k3sPodListStatus "$openlitespeedKube" || return 1
|
||||
|
||||
local pod phase output cpu mem count domain
|
||||
{
|
||||
printf '# HELP lsphp_cpu_percent Total CPU percent used by lsphp workers per domain\n'
|
||||
printf '# TYPE lsphp_cpu_percent gauge\n'
|
||||
printf '# HELP lsphp_memory_percent Total memory percent used by lsphp workers per domain\n'
|
||||
printf '# TYPE lsphp_memory_percent gauge\n'
|
||||
printf '# HELP lsphp_worker_count Number of lsphp worker processes per domain\n'
|
||||
printf '# TYPE lsphp_worker_count gauge\n'
|
||||
|
||||
while IFS='|' read -r pod phase; do
|
||||
[[ "$phase" == "Running" ]] || continue
|
||||
run output error openlitespeedPodExec "$pod" sh -c "ps aux | grep lsphp | grep -v grep | awk '{u=\$1;cpu[u]+=\$3;mem[u]+=\$4;count[u]++} END {for(u in cpu){name=u;cmd=\"find /var/www/data -maxdepth 1 -uid \"u\" -type d 2>/dev/null\";if((cmd|getline dir)>0&&dir!=\"\"){n=split(dir,a,\"/\");name=a[n]};close(cmd);print cpu[u],mem[u],count[u],name}}'" || continue
|
||||
while IFS=' ' read -r cpu mem count domain; do
|
||||
[[ -n "$domain" ]] || continue
|
||||
printf 'lsphp_cpu_percent{domain="%s",pod="%s"} %s\n' "$domain" "$pod" "$cpu"
|
||||
printf 'lsphp_memory_percent{domain="%s",pod="%s"} %s\n' "$domain" "$pod" "$mem"
|
||||
printf 'lsphp_worker_count{domain="%s",pod="%s"} %s\n' "$domain" "$pod" "$count"
|
||||
done <<< "$output"
|
||||
done < <(awk 'NF' <<< "$podList")
|
||||
} > "$fileTmp"
|
||||
|
||||
mv "$fileTmp" "$fileProm"
|
||||
}
|
||||
|
||||
# Collects disk usage per site and sends metrics to the API.
|
||||
function metricSites() {
|
||||
local metricSitesApiUrl="https://api.sodew.ai/api/metrics/sites"
|
||||
local batchId batchTime metricsJson dataJson payload httpCode
|
||||
batchId="$(uuidgen | tr '[:upper:]' '[:lower:]')"
|
||||
batchTime="$(date +%s)"
|
||||
metricsJson='{
|
||||
"site_disk_usage_mb": {"type":"number"}
|
||||
}'
|
||||
dataJson='{}'
|
||||
|
||||
local error vhostList
|
||||
run vhostList error openlitespeedVhostList all || {
|
||||
appError "Error retrieving vhost list: $error"
|
||||
return 1
|
||||
}
|
||||
while IFS= read -r domain <&3; do
|
||||
local diskUsage domainJson
|
||||
diskUsage="$(pathSize "$vhostsPath/$domain" "mb" || true)"
|
||||
[[ -n "$diskUsage" ]] || continue
|
||||
|
||||
domainJson="$(
|
||||
jq -n --arg diskUsage "$diskUsage" \
|
||||
'{
|
||||
site_disk_usage_mb: { value: (if ($diskUsage | length) > 0 then ($diskUsage | tonumber) else null end) },
|
||||
}'
|
||||
)"
|
||||
dataJson="$(jq --arg domain "$domain" --argjson row "$domainJson" '. + {($domain): $row}' <<< "$dataJson")"
|
||||
done 3< <(awk 'NF' <<< "$vhostList")
|
||||
|
||||
payload="$(
|
||||
jq -n \
|
||||
--arg source_type "worker" \
|
||||
--arg source_id "$hostUuid" \
|
||||
--arg batch_id "$batchId" \
|
||||
--argjson batch_time "$batchTime" \
|
||||
--argjson metrics "$metricsJson" \
|
||||
--argjson data "$dataJson" \
|
||||
'{
|
||||
source_type: $source_type,
|
||||
source_id: $source_id,
|
||||
batch_id: $batch_id,
|
||||
batch_time: $batch_time,
|
||||
metrics: $metrics,
|
||||
data: $data
|
||||
}'
|
||||
)"
|
||||
|
||||
# Sending data...
|
||||
httpCode="$(curl -sS -o /tmp/metrics_sites_response.txt -w '%{http_code}' -X POST "$metricSitesApiUrl" -H 'Content-Type: application/json' --data-binary "$payload")"
|
||||
if [[ "$httpCode" != "204" ]]; then
|
||||
appError "Ingest failed, HTTP $httpCode"
|
||||
cat /tmp/metrics_sites_response.txt >&2 || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user