This repository has been archived by the owner on Jan 24, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pbr.local
executable file
·376 lines (301 loc) · 11.7 KB
/
pbr.local
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
#!/usr/bin/env bash
show_help() {
echo "# pbr.sh — provisiong & backup & restore cycle for small hosts"
echo
echo "> pbr.local — local machine pbr.sh"
echo
echo "Usage: $0 {init-secrets|host-setup|host-backup|host-restore|help}"
echo
echo "Usage example:"
echo
echo "1) Create secrets folder in project directory"
echo
echo " pbr.local init-secrets ~/my-project"
echo
echo " # As result: ~/.my-project/pbr.secrets/ folder will be created"
echo
echo "2) Do configured remote host setup and provisiong"
echo
echo " pbr.local host-setup ./my-project/pbr.secrets/"
echo
echo "3) Backup remote host"
echo
echo " pbr.local host-backup ./my-project/pbr.secrets/"
echo
echo "4) Restore remote host"
echo
echo " pbr.local host-restore ./my-project/pbr.secrets/"
echo
echo "Other commands:"
echo
echo " pbr.local help - display help message"
echo
}
set -e
set -o pipefail
umask o=rwx,go=
# TUI text color helpers
echo_ok() { echo -e '\033[1;32m'"$1"'\033[0m'; }
echo_err() { echo -e '\033[1;31mERROR: '"$1"'\033[0m'; }
echo_warn() { echo -e '\033[1;33m'"$1"'\033[0m'; }
# Write error to stdout and exit
fatal_err() {
echo_err "$1"
exit 1
}
check_local_setup() {
script_dir=$(dirname $0)
script_dir=$(realpath "$script_dir")
[ -n "$1" ] || {
show_help
fatal_err "provide secrets directory with second argument in pbr.local"
}
secrets_arg=$(realpath "$1")
# if path provided then use default settings file name
if [ -d "$secrets_arg" ]; then
secrets_dir="$secrets_arg"
config_path="$secrets_dir/settings.conf"
# otherwise, if config file provided use this file
elif [ -f "$secrets_arg" ]; then
secrets_dir=$(dirname "$secrets_arg")
secrets_dir=$(realpath "$secrets_dir")
config_path="$secrets_arg"
else
fatal_err "$1 not a secrets directory nor secrets settings file"
fi
source "$config_path"
# check all required variables defined in configuration file
required_variables=(remote_user ssh_connection_string)
for var in "${required_variables[@]}"; do
test -n "${!var}" || fatal_err "$var not defined in $config_path"
done
# check for all required server configuration files are present
sshd_config_path="${script_dir}/common/sshd_config"
[ -f "$sshd_config_path" ] || fatal_err "$sshd_config_path missing"
# check backup script
backup_script_path="${script_dir}/pbr.host"
[ -f "$backup_script_path" ] || fatal_err "$backup_script_path missing"
# check local user public key
local_public_key_path=${local_public_key_path:-~/.ssh/id_rsa.pub}
[ -f "${local_public_key_path}" ] || \
fatal_err "local user public key not found at $local_public_key_path"
# check local user rclone config
local_rclone_config_path=${local_rclone_config_path:-~/.config/rclone/rclone.conf}
[ -f "${local_rclone_config_path}" ] || \
fatal_err "local user rclone config not found at $local_rclone_config_path"
# set backup cron time
host_backup_cron_time=${backup_cron_time:-"30 0 * * *"}
[ -n "$host_backup_cron_time" ] || fatal_err "Cron time not confgiured"
}
check_remote_conntection() {
# test connection to remote host via ssh
if ! (ssh -q "$ssh_connection_string" exit); then
fatal_err "Connection to remote via ssh $ssh_connection_string failed"
fi
}
check_backup_config() {
if [ -n "$backup_name" ]; then
# check volumes setting
[ -n "$docker_backup_volumes" ] && [ -z "$data_rclone_repo" ] && \
fatal_err "docker_backup_volumes defined, but data_rclone_repo isn't"
# check docker mysql settings
if [ -n "$docker_mysql_db_names" ]; then
[ -z "$db_rclone_repo" ] && {
fatal_err "docker_mysql_db_names defined, but db_rclone_repo isn't"; }
test -z "$docker_mysql_user" && fatal_err "docker_mysql_user not defined"
# warn on empty password, do not fail
[ -z "$docker_mysql_password" ] && { echo_warn "docker_mysql_password not defined (or empty)"; }
fi
# check host postgresql settings
if [ -n "$host_postgresql_db_names" ]; then
[ -z "$db_rclone_repo" ] && fatal_err "host_postgresql_db_names defined, but db_rclone_repo isn't"
[ -z "$host_postgresql_user"] && fatal_err "host_postgresql_user not defined"
# test -z "$host_postgresql_password" && { echo_err "host_postgresql_password not defined" && exit 1; }
fi
backup_enabled=1
echo_ok "Backups enabled"
else
backup_enabled=0
echo_err "Backups disabled, you can enable it via backup_name setting in $config_path"
fi
# If not all telegram settings defined -> ignore but warn, this is optional
if ! [ -n "$telegram_chat_id" ] && [ -n "$telegram_bot_token" ]; then
echo_err "Telegram notifications disabled, please define telegram_chat_id and telegram_bot_token in $config_path";
fi
}
do_host_backup() {
ssh -l $remote_user $ssh_connection_string ./pbr.host backup
}
do_host_restore() {
ssh -l $remote_user $ssh_connection_string ./pbr.host restore
}
do_host_setup() {
if [ -z "$remote_user_password" ]; then
# Ask for remote user password if password not defined in config file
read -s -p "Enter remote user password: " remote_user_password
fi
# Copy all configs and scripts to remote
scp "$backup_script_path" \
"$sshd_config_path" \
"$local_public_key_path" \
"$local_rclone_config_path" \
"$ssh_connection_string:."
scp -r $secrets_dir/* $ssh_connection_string:.secrets/
# Copy password via ssh to remote host
# on remote host get password from stdin and write it to file with limited permissions
echo $remote_user_password | \
ssh $ssh_connection_string 'cat > remote_user_password && chmod 400 remote_user_password'
# Construct docker daemon configuration with user namespace remapping enabled
# If no daemon.json file stored in secrets folder
if ! [ -r "$secrets_dir/daemon.json" ]; then
daemon_config="{\"userns-remap\":\"$remote_user\"}"
echo $daemon_config | \
ssh $ssh_connection_string 'cat > daemon.json'
fi
# Run setup on remote host
ssh "$ssh_connection_string" /bin/bash <<- STR
set -e
set -o pipefail
# uploaded file for cleanup:
working_files=(remote_user_password $(basename $local_public_key_path) daemon.json rclone.conf pbr.host sshd_config)
finalize() {
for file in "\${working_files[@]}"; do
# echo "removing working file \$file"
rm -rf "\$file"
done
working_files=()
}; trap finalize EXIT ERR
source .secrets/$(basename $config_path)
should_create_user=${host_create_user:-'true'}
should_recreate_ssh_key=${host_recreate_ssh_key:-'false'}
should_host_docker_nsremap=${host_docker_nsremap:-'true'}
should_restart_services=${host_restart_services:-'false'}
# Update distro and setup required toolset
apt-get update
apt-get --yes --force-yes upgrade
apt-get --yes --force-yes install rclone restic docker.io neovim
# Create our user on host machine:
if [ \$should_create_user = 'true' ]; then
if useradd -m -U $remote_user; then
# Get password hash and set it
# also add our user to docker group
hashed_password=\$(openssl passwd -6 \$(cat ./remote_user_password))
usermod -a -G docker -p \$hashed_password -s /bin/bash $remote_user
fi
fi
if [ \$should_host_docker_nsremap = 'true' ]; then
uids_mapping=\$(cat /etc/subuid)
echo "remap userns"
# default uids mapping for ubuntu will be
#
# username:100000:65536
#
# we will map docker root to our host user via appending line
#
# username:1000:1
#
# after what we will swap lines order, so 1000:1 will be first
if ! [[ "$uids_mapping" =~ ^$remote_user:1000:1 ]]; then
echo "remap userns A"
# user namespace remapping for containers
usermod -v 1000-1000 -w 1000-1000 $remote_user
tac /etc/subuid > ./temp_subuid
cat ./temp_subuid > /etc/subuid
tac /etc/subgid > ./temp_subgid
cat ./temp_subgid > /etc/subgid
rm -f ./temp_subuid ./temp_subgid
fi
fi
if [ \$should_recreate_ssh_key = 'true' ] || ! [ -f /home/$remote_user/.ssh/id_rsa.pub ]; then
# Generate keys pair on host machine
su $remote_user -c "ssh-keygen -P \$(cat ./remote_user_password) -f ~/.ssh/id_rsa"
fi
# Mark host machine
su $remote_user -c "touch ~/.pbr-host && chmod 400 ~/.pbr-host"
# Add local public key to host user authorized keys
local_pkey=\$(cat $(basename "$local_public_key_path"))
current_authorized=\$(cat /home/$remote_user/.ssh/authorized_keys)
if ! [[ "\$current_authorized" =~ "\$local_pkey" ]]; then
echo \$local_pkey >> /home/$remote_user/.ssh/authorized_keys
fi
# Copy rclone config to host user configs
su $remote_user -c 'mkdir -p ~/.config/rclone;'
mv $(basename "$local_rclone_config_path") /home/$remote_user/.config/rclone/
# Set restictive access to .config folders
chmod -R 700 /home/$remote_user/.config
chown -R $remote_user:$remote_user /home/$remote_user/.config
# Move backup script to host user home dir
chmod 500 ./pbr.host
chown $remote_user:$remote_user ./pbr.host
mv ./pbr.host /home/$remote_user/
# Move server configs to specified paths
mv ./sshd_config /etc/ssh/sshd_config
[ -f ./daemon.json ] && mv ./daemon.json /etc/docker/daemon.json
[ -f ./.secrets/daemon.json ] && mv ./.secrets/daemon.json /etc/docker/daemon.json
# Move secrets to user home dir and set restrictive access
su $remote_user -c 'mkdir -p ~/.config/.secrets;'
chmod -R 500 ./.secrets
chown -R $remote_user:$remote_user ./.secrets
mv ./.secrets/* /home/$remote_user/.config/.secrets/
working_files+=(./secrets)
# Set backup script execution via cron
current_cron=\$(crontab -l -u $remote_user)
if ! [[ "\$current_cron" =~ "pbr.host backup" ]]; then
echo \$current_cron > cronfile
echo "$host_backup_cron_time /home/$remote_user/pbr.host backup" >> cronfile
crontab -u $remote_user cronfile; rm -f cronfile
fi
# Restart services, so new uploaded configs will apply
if [ \"\$should_restart_services\" = 'true' ]; then
echo 'Restarting services'
service sshd restart
service docker restart
fi
echo "Setup complete"
STR
}
init_config() {
[ -z "$1" ] && fatal_err "secrets path not provided via argument"
[ -d "$1" ] || fatal_err "provided path not a directory: $1"
local secrets_folder_name
if [ -n "$2" ]; then
secrets_folder_name="$2"
else
secrets_folder_name="pbr.secrets"
fi
script_dir=$(dirname $0)
possible_path="$(realpath "$1")/$secrets_folder_name"
proto_path="${script_dir}/pbr.secrets.example"
[ -d "$possible_path" ] && \
fatal_err "secrets directory on $possible_path already initialized"
[ -d "$proto_path" ] || \
fatal_err "proto cofig missing under $proto_path"
mkdir -v $possible_path
cp -vr $proto_path/* $possible_path
echo_warn "Secrets folder initialized in $possible_path"
echo_warn "Please don't forget to add pbr.secrets folder to .gitignore"
}
# Execution part
echo_ok "Running pbr.sh (on local machine)"
if [ "$1" = "host-setup" ]; then
echo_warn "Checking configuration"
check_local_setup "$2"
check_remote_conntection
check_backup_config
echo_ok "Checking complete"
do_host_setup
elif [ "$1" = "init-secrets" ]; then
init_config "$2" "$3"
elif [ "$1" = "host-backup" ]; then
check_local_setup "$2"
do_host_backup
elif [ "$1" = "host-restore" ]; then
check_local_setup "$2"
do_host_restore
elif [ "$1" = "help" ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
show_help
else
echo_err "Action not provided via command line\n"
show_help
exit 1
fi