Subversion Repositories ALCASAR

Rev

Rev 3314 | Rev 3319 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log

Rev Author Line No. Line
3294 rexy 1
#!/bin/bash
2
 
3
#########################
4
## ALCASAR replication ##
5
##       connect       ##
6
#########################
7
# The script is designed to connect instance to a remote ALCASAR.
8
 
9
# Constants
10
readonly ALCASAR_PWD="/root/ALCASAR-passwords.txt"
11
readonly LOCALHOST="127.0.0.1"
12
readonly DB_PORT=3306
13
 
14
# Dynamically generated constants
15
DB_ROOT_PWD="$(grep db_root "$ALCASAR_PWD" | cut -d '=' -f 2-)"
3318 rexy 16
REPL_DB_USER_PWD="$(grep db_replication_pwd "$ALCASAR_PWD" | cut -d '=' -f 2-)"
3294 rexy 17
readonly DB_ROOT_PWD;
3318 rexy 18
readonly REPL_DB_USER=db_replication
19
readonly REPL_DB_USER_PWD;
3294 rexy 20
 
21
# Variables
22
remote_name=""
23
remote_addr=""
24
remote_ssh_port=""
25
remote_ssh_user=""
26
remote_db_user=""
27
remote_db_pwd=""
28
remote_role=""
29
bind_port=""
30
 
31
 
32
# Revert modifications already made while adding remote
33
# $1: previous error code
34
abort() {
35
	error_code="$1"
3318 rexy 36
	echo "Abort script with $error_code error code"
3294 rexy 37
	# Revert FW
38
	tmp_disable_outbound_connection
3318 rexy 39
	# Remove REPLICA
40
	del_remote_as_primary
3294 rexy 41
	# Delete SSH tunnel service file
42
	service_file="replication-$remote_name.service"
43
	service_path="/etc/systemd/system/$service_file"
44
	[ -f "$service_file" ] && rm "$service_file"
45
	return "$error_code"
46
}
47
 
48
# Add remote as primary
49
add_remote_as_primary() {
50
	echo "Adding '$remote_name' as primary..."
51
	exec_query "CHANGE MASTER '$remote_name' TO MASTER_HOST='$LOCALHOST', MASTER_PORT=$bind_port, MASTER_USER='$remote_db_user', MASTER_PASSWORD='$remote_db_pwd', MASTER_USE_GTID=replica_pos"
52
}
53
 
3318 rexy 54
# Delete remote as primary
55
del_remote_as_primary() {
56
	echo "Removing '$remote_name' as primary..."
57
	exec_query "RESET REPLICA '$remote_name' ALL"
58
}
59
 
3294 rexy 60
# Verify hostname and IP are not already used by other primary servers
61
check_availability() {
62
	attributes="$(/usr/local/bin/alcasar-replication-list.sh --all)"
63
 
64
	# Check for remote name availability
65
	echo "$attributes" | grep -q "$remote_name"
66
	if [ "$?" -eq 0 ]
67
	then
68
		echo "error: name '$remote_name' already used" >&2
69
		return 15
70
	fi
71
 
72
	# Check for remote IP availability
73
	echo "$attributes" | grep -q "$remote_addr"
74
	if [ "$?" -eq 0 ] && [ -n "$remote_addr" ]
75
	then
76
		echo "error: address '$remote_addr' already used" >&2
77
		return 16
78
	fi
79
 
80
	# Check for binding port availability
81
	echo "$attributes" | grep -q "$bind_port"
82
	if [ "$?" -eq 0 ] && [ -n "$bind_port" ]
83
	then
84
		echo "error: binding port '$bind_port' already used" >&2
85
		return 17
86
	fi
87
}
88
 
89
# Check script args
90
# $@: script args
91
check_args() {
92
	# Parse args
93
	args="$(getopt --longoptions "to-primary,to-secondary,name:,address:,port:,user:,db-user:,db-password:,bind-port:,help" --options "n:,a:,p:,u:,h" -- "$@")"
94
 
95
	# Reset script args list
96
	eval set -- "$args"
97
 
98
	# Print help
99
	if [ "$#" -eq 1 ]
100
	then
101
		usage
102
		return 1
103
	fi
104
 
105
	# Loop over all args
106
	while true
107
	do
108
		case "$1" in
109
			--to-primary)
110
				echo "Remote role: primary"
111
				remote_role="primary"
112
				;;
113
			--to-secondary)
114
				echo "Remote role: secondary"
115
				remote_role="secondary"
116
				;;
117
			--name | -n)
118
				echo "Remote name: $2"
119
				remote_name="$2"
120
				shift
121
				;;
122
			--address | -a)
123
				echo "Remote address: $2"
124
				remote_addr="$2"
125
				shift
126
				;;
127
			--port | -p)
128
				echo "Remote SSH port: $2"
129
				remote_ssh_port="$2"
130
				shift
131
				;;
132
			--user | -u)
133
				echo "Remote user: $2"
134
				remote_ssh_user="$2"
135
				shift
136
				;;
137
			--db-user)
138
				echo "Remote database user: $2"
139
				remote_db_user="$2"
140
				shift
141
				;;
142
			--db-password)
143
				echo "Remote database user password: $2"
144
				remote_db_pwd="$2"
145
				shift
146
				;;
147
			--bind-port)
148
				echo "Local binding port: $2"
149
				bind_port="$2"
150
				shift
151
				;;
152
			--help | -h)
153
				usage
154
				return 2
155
				;;
156
			--)
157
				# End of args
158
				break
159
				;;
160
			*)
161
				echo "error: unknown $1" >&2
162
				return 3
163
				break
164
				;;
165
		esac
166
		shift
167
	done
168
 
169
	# All fields must be filled
170
	case "$remote_role" in
171
		primary)
172
			# Needed args to be passed
173
			if [ -z "$remote_name"     ] ||
174
			   [ -z "$remote_addr"     ] ||
175
			   [ -z "$remote_ssh_port" ] ||
176
			   [ -z "$remote_ssh_user" ] ||
177
			   [ -z "$remote_db_user"  ] ||
178
			   [ -z "$remote_db_pwd"   ]
179
			then
180
				echo "error: some args are missing" >&2
181
				return 4
182
			fi
183
			;;
184
		secondary)
185
			# Needed args to be passed
186
			if [ -z "$remote_name"     ] ||
187
			   [ -z "$bind_port"       ] ||
188
			   [ -z "$remote_db_user"  ] ||
189
			   [ -z "$remote_db_pwd"   ]
190
			then
191
				echo "error: some args are missing" >&2
192
				return 5
193
			fi
194
			;;
195
		*)
196
			echo "error: remote role is missing" >&2
197
			return 6
198
			;;
199
	esac
200
}
201
 
202
# Test connection to remote system and remote database
203
# before making SSH tunnel.
204
check_primary_credentials() {
205
	# Test SSH credentials
206
	if ! /usr/bin/ssh -o StrictHostKeyChecking=no -o PasswordAuthentication=no -p "$remote_ssh_port" "$remote_ssh_user"@"$remote_addr" exit
207
	then
208
		echo "error: cannot SSH with '$remote_ssh_user' to $remote_addr:$remote_ssh_port" >&2
209
		echo "hint: have you deployed root pubkey on the remote?"
210
		return 7
211
	fi
212
 
3314 rexy 213
	echo "Successfully connected with '$remote_ssh_user' to primary ($remote_addr:$remote_ssh_port)"
3294 rexy 214
 
215
	# Test database credentials
216
	if ! /usr/bin/ssh -q -p "$remote_ssh_port" "$remote_ssh_user"@"$remote_addr" -- /usr/bin/mariadb --user="$remote_db_user" --password="$remote_db_pwd" --execute="QUIT"
217
	then
218
		echo "error: cannot connect with '$remote_db_user' to remote database" >&2
219
		return 8
220
	fi
221
 
222
	echo "Successfully connected with '$remote_db_user' to remote database"
223
}
224
 
225
# Test connection to remote database through SSH tunnel
226
check_secondary_credentials() {
227
	if ! /usr/bin/mariadb --host="$LOCALHOST" --port="$bind_port" --user="$remote_db_user" --password="$remote_db_pwd" --execute="QUIT"
228
	then
229
		echo "error: cannot connect with '$remote_db_user' to remote database" >&2
230
		return 9
231
	fi
3314 rexy 232
	echo "Successfully connected with '$remote_db_user' to remote secondary database on port $bind_port"
3294 rexy 233
}
234
 
235
# Make a SSH tunnel to remote host
236
create_ssh_tunnel() {
237
	# Find a common binding port
238
	find_common_free_port || return 11
239
	service_file="replication-$remote_name.service"
240
	service_path="/etc/systemd/system/$service_file"
241
 
242
	# Write down SSH tunnel service file
243
	echo "[Unit]
244
Description=Setup a secure bidirectional tunnel with $remote_name
245
After=network.target
246
 
247
[Service]
248
ExecStart=/usr/bin/ssh -NT -4 -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -p $remote_ssh_port -L $bind_port:localhost:$DB_PORT -R $bind_port:localhost:$DB_PORT $remote_ssh_user@$remote_addr
249
RestartSec=5
250
Restart=always
251
 
252
[Install]
253
WantedBy=multi-user.target
254
" > "$service_path"
255
 
256
	# Start and enable SSH tunnel
257
	echo "Enabling $remote_name service..."
258
	/usr/bin/systemctl enable "$service_file"
259
	echo "Starting $remote_name service..."
260
	/usr/bin/systemctl start "$service_file"
261
}
262
 
263
# Execute SQL query
264
# $1: query
265
# $2: user (default: root)
266
# $3: password (default: root pwd)
267
# $4: host (default: localhost)
268
# $5: port (default: 3306)
269
exec_query() {
270
	# Check args
271
	if [ $# -lt 1 ]
272
	then
273
		echo "usage: $0 \"SQL query\" <DB user> <DB password> <SQL server address> <SQL server port>"
274
		return 12
275
	fi
276
	# Execute the query
277
	/usr/bin/mariadb --host="${4:-localhost}" --port="${5:-$DB_PORT}" --user="${2:-root}" --password="${3:-$DB_ROOT_PWD}" --execute="$1"
278
}
279
 
3318 rexy 280
retrieve_primary_data() {
281
	# creation of a fresh dump
282
	if ! /usr/bin/ssh -q -p "$remote_ssh_port" "$remote_ssh_user"@"$remote_addr" -- "sudo /usr/local/bin/alcasar-mariadb.sh -d &&  cp -f /var/Save/base/\$(ls -t /var/Save/base/ | head -n 1) /home/replication/alcasar-users-database-primary.sql.gz"
283
	then
284
		echo "error: cannot create a fresh primary database dump" >&2
285
		return 18
286
	fi
287
	echo "Primary database dump created"
288
	if ! scp -q -P "$remote_ssh_port" "$remote_ssh_user"@"$remote_addr":alcasar-users-database-primary.sql.gz "$remote_ssh_user"@"$remote_addr":db_replication_user_pass.txt /tmp
289
	then
290
		echo "error: cannot retrieve localy the fresh primary database dump" >&2
291
		return 19
292
	fi
293
	echo "Primary database dump locally copied"
294
	readalcasar-mariadb.sh --import /tmp/alcasar-users-database-primary.sql.gz
295
	rm -f /tmp/alcasar-users-database-primary.sql.gz
296
}
297
 
3294 rexy 298
find_common_free_port() {
299
	remote_busy_ports_file=/tmp/remote_busy_ports
300
	local_busy_ports_file=/tmp/local_busy_ports
301
	common_busy_ports_file=/tmp/common_busy_ports
302
	ports_list_file=/tmp/ports_list
303
	free_ports_file=/tmp/free_ports
304
 
305
	# Get remote busy ports
306
	/usr/bin/ssh -q -p "$remote_ssh_port" "$remote_ssh_user"@"$remote_addr" -- /usr/sbin/ss --listening --numeric --ipv4 | tail -n +2 | cut -d ':' -f 2 | cut -d ' ' -f 1 | sort -u > "$remote_busy_ports_file"
307
	if [ "$?" -ne 0 ]
308
	then
309
		echo "error: cannot SSH with '$remote_ssh_user' to $remote_addr:$remote_ssh_port" >&2
310
		return 13
311
	fi
312
 
313
	# Get local busy ports
314
	/usr/sbin/ss --listening --numeric --ipv4 | tail -n +2 | cut -d ':' -f 2 | cut -d ' ' -f 1 | sort -u > "$local_busy_ports_file"
315
 
316
	# List ports range from system
317
	read lower_port upper_port < /proc/sys/net/ipv4/ip_local_port_range
318
 
319
	# Write ports in a file
320
	echo -n > "$ports_list_file"
321
	for port in $(seq "$lower_port" "$upper_port")
322
	do
323
		echo "$port" >> "$ports_list_file"
324
	done
325
 
326
	# Merge busy ports
327
	/usr/bin/cat "$remote_busy_ports_file" "$local_busy_ports_file" > "$common_busy_ports_file"
328
	# Sorts ports
329
	/usr/bin/sort -u -o "$common_busy_ports_file" "$common_busy_ports_file"
330
	/usr/bin/sort -o "$ports_list_file" "$ports_list_file"
331
	# Substract available ports in common
332
	/usr/bin/comm --check-order -3 "$ports_list_file" "$common_busy_ports_file" | cut -f 1 | sed "/^$/d" > "$free_ports_file"
333
 
334
	# Verify at least one free port have been found
335
	if [ ! -s "$free_ports_file" ]
336
	then
337
		echo "error: no common port found for binding" >&2
338
		return 14
339
	fi
340
 
341
	# Pick the first common port
342
	bind_port="$(head -n 1 "$free_ports_file")"
343
	echo "Both machines binded on port $bind_port->$DB_PORT"
344
	echo "Please take note about the binding port for primary's connection setup."
345
 
346
	# Remove tmp files
347
	rm "$remote_busy_ports_file"
348
	rm "$local_busy_ports_file"
349
	rm "$common_busy_ports_file"
350
	rm "$ports_list_file"
351
	rm "$free_ports_file"
352
}
353
 
354
# Allow outbound connection for testing connection
355
tmp_allow_outbound_connection() {
356
	/usr/sbin/iptables -A OUTPUT -d "$remote_addr" -p tcp --dport "$remote_ssh_port" -j ACCEPT
357
}
358
 
3313 rexy 359
# Disable outbound connection which that was used to test test connection
3294 rexy 360
tmp_disable_outbound_connection() {
361
	/usr/sbin/iptables -D OUTPUT -d "$remote_addr" -p tcp --dport "$remote_ssh_port" -j ACCEPT
362
}
363
 
364
# Print help message
365
usage() {
366
	echo "usage: $0 ROLE OPTIONS"
367
	echo
368
	echo "ROLE"
369
	echo "	--to-primary"
370
	echo "		remote server is a primary"
371
	echo "	--to-secondary"
372
	echo "		remote server is a secondary"
373
	echo
374
	echo "OPTIONS"
375
	echo "	--name=NAME, -n NAME"
376
	echo "		friendly name given to the remote"
377
	echo "	--address=ADDRESS, -a ADDRESS"
378
	echo "		remote IP address"
379
	echo "	--port=PORT, -p PORT"
380
	echo "		remote SSH port"
381
	echo "	--user=USER, -u USER"
382
	echo "		remote SSH user"
383
	echo "	--db-user=USER"
384
	echo "		remote database replication user"
385
	echo "	--db-password=PASSWORD"
386
	echo "		remote database replication user password"
387
	echo "	--bind-port=PORT"
388
	echo "		used from primary: local port binded to remote database. It has been displayed during secondary connection to primary"
389
	echo "	--help, -h"
390
	echo "		print this help message"
391
	echo
392
	echo "ROLE OPTIONS"
393
	echo "	--to-primary: needs name, address, port, user, db-user, db-password"
394
	echo "	--to-secondary: needs name, bind-port, db-user, db-password"
395
}
396
 
397
# Main
398
check_args "$@" || exit
399
 
400
check_availability || exit
401
 
402
case "$remote_role" in
403
	primary)
404
		tmp_allow_outbound_connection || abort "$?" || exit
405
		check_primary_credentials || abort "$?" || exit
406
		create_ssh_tunnel || abort "$?" || exit
3318 rexy 407
		retrieve_primary_data || abort "$?" || exit
3314 rexy 408
		add_remote_as_primary || abort "$?" || exit
409
		echo -n "Allowing outbound connection to remote SSH "
410
		# Get remote IP and port from its name
411
		port="$(grep "ExecStart" "$service_path" | cut -d ' ' -f 9)"
412
		ip="$(grep "ExecStart" "$service_path" | cut -d ' ' -f 14 | cut -d '@' -f2)"
413
		echo "($ip:$port)"
414
		/usr/bin/sed -i -E "/^REPLICATION_TO=/s/=(.*)/=\1$ip:$port,/" /usr/local/etc/alcasar.conf
415
		/usr/local/bin/alcasar-iptables.sh
3294 rexy 416
		;;
417
	secondary)
418
		check_secondary_credentials || exit
3314 rexy 419
		add_remote_as_primary || abort "$?" || exit # In a federation, primary/secondary is define by SSH role (sshd-server=primary; ssh-client=secondary)
3294 rexy 420
		;;
421
esac
422
 
3313 rexy 423
# Set Netfilter
424
 
3314 rexy 425