Subversion Repositories ALCASAR

Rev

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