Subversion Repositories ALCASAR

Rev

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