ctdb-scripts: Quote some variable expansions
[nivanova/samba-autobuild/.git] / ctdb / config / events.d / 13.per_ip_routing
1 #!/bin/sh
2
3 [ -n "$CTDB_BASE" ] || \
4     CTDB_BASE=$(d=$(dirname "$0") ; cd -P "$d" ; dirname "$PWD")
5
6 . "${CTDB_BASE}/functions"
7
8 loadconfig
9
10 service_name=per_ip_routing
11
12 # Do nothing if unconfigured 
13 [ -n "$CTDB_PER_IP_ROUTING_CONF" ] || exit 0
14
15 table_id_prefix="ctdb."
16
17 [ -n "$CTDB_PER_IP_ROUTING_RULE_PREF" ] || \
18     die "error: CTDB_PER_IP_ROUTING_RULE_PREF not configured"
19
20 [ "$CTDB_PER_IP_ROUTING_TABLE_ID_LOW" -lt "$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH" ] 2>/dev/null || \
21     die "error: CTDB_PER_IP_ROUTING_TABLE_ID_LOW[$CTDB_PER_IP_ROUTING_TABLE_ID_LOW] and/or CTDB_PER_IP_ROUTING_TABLE_ID_HIGH[$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH] improperly configured"
22
23 if [ "$CTDB_PER_IP_ROUTING_TABLE_ID_LOW" -le 253 -a \
24     255 -le "$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH" ] ; then
25     die "error: range CTDB_PER_IP_ROUTING_TABLE_ID_LOW[$CTDB_PER_IP_ROUTING_TABLE_ID_LOW]..CTDB_PER_IP_ROUTING_TABLE_ID_HIGH[$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH] must not include 253-255"
26 fi
27
28 have_link_local_config ()
29 {
30     [ "$CTDB_PER_IP_ROUTING_CONF" = "__auto_link_local__" ]
31 }
32
33 if ! have_link_local_config && [ ! -r "$CTDB_PER_IP_ROUTING_CONF" ] ; then
34     die "error: CTDB_PER_IP_ROUTING_CONF=$CTDB_PER_IP_ROUTING_CONF file not found"
35 fi
36
37 ######################################################################
38
39 ipv4_is_valid_addr()
40 {
41     _ip="$1"
42
43     _count=0
44     # Get the shell to break up the address into 1 word per octet 
45     for _o in $(export IFS="." ; echo $_ip) ; do
46         # The 2>/dev/null stops output from failures where an "octet"
47         # is not numeric.  The test will still fail.
48         if ! [ 0 -le $_o -a $_o -le 255 ] 2>/dev/null ; then
49             return 1
50         fi
51         _count=$(($_count + 1))
52     done
53
54     # A valid IPv4 address has 4 octets
55     [ $_count -eq 4 ]
56 }
57
58 ensure_ipv4_is_valid_addr ()
59 {
60     _event="$1"
61     _ip="$2"
62
63     ipv4_is_valid_addr "$_ip" || {
64         echo "$0: $_event not an ipv4 address skipping IP:$_ip"
65         exit 0
66     }
67 }
68
69 ipv4_host_addr_to_net ()
70 {
71     _host="$1"
72     _maskbits="$2"
73
74     # Convert the host address to an unsigned long by splitting out
75     # the octets and doing the math.
76     _host_ul=0
77     for _o in $(export IFS="." ; echo $_host) ; do
78         _host_ul=$(( ($_host_ul << 8) + $_o)) # work around Emacs color bug
79     done
80
81     # Calculate the mask and apply it.
82     _mask_ul=$(( 0xffffffff << (32 - $_maskbits) ))
83     _net_ul=$(( $_host_ul & $_mask_ul ))
84  
85     # Now convert to a network address one byte at a time.
86     _net=""
87     for _o in $(seq 1 4) ; do
88         _net="$(($_net_ul & 255))${_net:+.}${_net}"
89         _net_ul=$(($_net_ul >> 8))
90     done
91
92     echo "${_net}/${_maskbits}"
93 }
94
95 ######################################################################
96
97 ensure_rt_tables ()
98 {
99     rt_tables="$CTDB_SYS_ETCDIR/iproute2/rt_tables"
100
101     # This file should always exist.  Even if this didn't exist on the
102     # system, adding a route will have created it.  What if we startup
103     # and immediately shutdown?  Let's be sure.
104     if [ ! -f "$rt_tables" ] ; then
105         mkdir -p "${rt_tables%/*}" # dirname
106         touch "$rt_tables"
107     fi
108 }
109
110 # Setup a table id to use for the given IP.  We don't need to know it,
111 # it just needs to exist in /etc/iproute2/rt_tables.  Fail if no free
112 # table id could be found in the configured range.
113 ensure_table_id_for_ip ()
114 {
115     _ip=$1
116
117     ensure_rt_tables
118
119     # Maintain a table id for each IP address we've ever seen in
120     # rt_tables.  We use a "ctdb." prefix on the label.
121     _label="${table_id_prefix}${_ip}"
122
123     # This finds either the table id corresponding to the label or a
124     # new unused one (that is greater than all the used ones in the
125     # range).
126     (
127         # Note that die() just gets us out of the subshell...
128         flock --timeout 30 0 || \
129             die "ensure_table_id_for_ip: failed to lock file $rt_tables"
130
131         _new=$CTDB_PER_IP_ROUTING_TABLE_ID_LOW
132         while read _t _l ; do
133             # Skip comments
134             case "$_t" in
135                 \#*) continue ;;
136             esac
137             # Found existing: done
138             if [ "$_l" = "$_label" ] ; then
139                 return 0
140             fi
141             # Potentially update the new table id to be used.  The
142             # redirect stops error spam for a non-numeric value.
143             if [ $_new -le $_t -a \
144                 $_t -le $CTDB_PER_IP_ROUTING_TABLE_ID_HIGH ] 2>/dev/null ; then
145                 _new=$(($_t + 1))
146             fi
147         done
148
149         # If the new table id is legal then add it to the file and
150         # print it.
151         if [ $_new -le $CTDB_PER_IP_ROUTING_TABLE_ID_HIGH ] ; then
152             printf "%d\t%s\n" "$_new" "$_label" >>"$rt_tables"
153             return 0
154         else
155             return 1
156         fi
157     ) <"$rt_tables"
158 }
159
160 # Clean up all the table ids that we might own.
161 clean_up_table_ids ()
162 {
163     ensure_rt_tables
164
165     (
166         # Note that die() just gets us out of the subshell...
167         flock --timeout 30 0 || \
168             die "clean_up_table_ids: failed to lock file $rt_tables"
169
170         # Delete any items from the file that have a table id in our
171         # range or a label matching our label.  Preserve comments.
172         _tmp="${rt_tables}.$$.ctdb"
173         awk -v min="$CTDB_PER_IP_ROUTING_TABLE_ID_LOW" \
174             -v max="$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH" \
175             -v pre="$table_id_prefix" \
176             '/^#/ || \
177              !(min <= $1 && $1 <= max) && \
178              !(index($2, pre) == 1) \
179              { print $0 }' "$rt_tables" >"$_tmp"
180
181         mv "$_tmp" "$rt_tables"
182         # The lock is gone - don't do anything else here
183     ) <"$rt_tables"
184 }
185
186 ######################################################################
187
188 # This prints the config for an IP, which is either relevant entries
189 # from the config file or, if set to the magic link local value, some
190 # link local routing config for the IP.
191 get_config_for_ip ()
192 {
193     _ip="$1"
194
195     if have_link_local_config ; then
196         # When parsing public_addresses also split on '/'.  This means
197         # that we get the maskbits as item #2 without further parsing.
198         while IFS="/$IFS" read _i _maskbits _x ; do
199             if [ "$_ip" = "$_i" ] ; then
200                 echo -n "$_ip "; ipv4_host_addr_to_net "$_ip" "$_maskbits"
201             fi
202         done <"${CTDB_PUBLIC_ADDRESSES:-/dev/null}"
203     else
204         while read _i _rest ; do
205             if [ "$_ip" = "$_i" ] ; then
206                 printf "%s\t%s\n" "$_ip" "$_rest"
207             fi
208         done <"$CTDB_PER_IP_ROUTING_CONF"
209     fi
210 }
211
212 ip_has_configuration ()
213 {
214     _ip="$1"
215
216     [ -n "$(get_config_for_ip $_ip)" ]
217 }
218
219 add_routing_for_ip ()
220 {
221     _iface="$1"
222     _ip="$2"
223
224     # Do nothing if no config for this IP.
225     ip_has_configuration "$_ip" || return 0
226
227     ensure_table_id_for_ip "$_ip" || \
228         die "add_routing_for_ip: out of table ids in range $CTDB_PER_IP_ROUTING_TABLE_ID_LOW - $CTDB_PER_IP_ROUTING_TABLE_ID_HIGH"
229
230     _pref="$CTDB_PER_IP_ROUTING_RULE_PREF"
231     _table_id="${table_id_prefix}${_ip}"
232
233     del_routing_for_ip "$_ip" >/dev/null 2>&1
234
235     ip rule add from "$_ip" pref "$_pref" table "$_table_id" || \
236         die "add_routing_for_ip: failed to add rule for $_ip"
237
238     # Add routes to table for any lines matching the IP.
239     get_config_for_ip "$_ip" |
240     while read _i _dest _gw ; do
241         _r="$_dest ${_gw:+via} $_gw dev $_iface table $_table_id"
242         ip route add $_r || \
243             die "add_routing_for_ip: failed to add route: $_r"
244     done
245 }
246
247 del_routing_for_ip ()
248 {
249     _ip="$1"
250
251     _pref="$CTDB_PER_IP_ROUTING_RULE_PREF"
252     _table_id="${table_id_prefix}${_ip}"
253
254     # Do this unconditionally since we own any matching table ids.
255     # However, print a meaningful message if something goes wrong.
256     _cmd="ip rule del from $_ip pref $_pref table $_table_id"
257     _out=$($_cmd 2>&1) || \
258         cat <<EOF
259 WARNING: Failed to delete policy routing rule
260   Command "$_cmd" failed:
261   $_out
262 EOF
263     # This should never usually fail, so don't redirect output.
264     # However, it can fail when deleting a rogue IP, since there will
265     # be no routes for that IP.  In this case it should only fail when
266     # the rule deletion above has already failed because the table id
267     # is invalid.  Therefore, go to a little bit of trouble to indent
268     # the failure message so that it is associated with the above
269     # warning message and doesn't look too nasty.
270     ip route flush table "$_table_id" 2>&1 | sed -e 's@^.@  &@'
271 }
272
273 ######################################################################
274
275 flush_rules_and_routes ()
276 {
277         ip rule show |
278         while read _p _x _i _x _t ; do
279             # Remove trailing colon after priority/preference.
280             _p="${_p%:}"
281             # Only remove rules that match our priority/preference.
282             [ "$CTDB_PER_IP_ROUTING_RULE_PREF" = "$_p" ] || continue
283
284             echo "Removing ip rule for public address $_i for routing table $_t"
285             ip rule del from "$_i" table "$_t" pref "$_p"
286             ip route flush table "$_t" 2>/dev/null
287         done
288 }
289
290 # Add any missing routes.  Some might have gone missing if, for
291 # example, all IPs on the network were removed (possibly if the
292 # primary was removed).  If $1 is "force" then (re-)add all the
293 # routes.
294 add_missing_routes ()
295 {
296     $CTDB ip -v -X | {
297         read _x # skip header line
298
299         # Read the rest of the lines.  We're only interested in the
300         # "IP" and "ActiveInterface" columns.  The latter is only set
301         # for addresses local to this node, making it easy to skip
302         # non-local addresses.  For each IP local address we check if
303         # the relevant routing table is populated and populate it if
304         # not.
305         while IFS="|" read _x _ip _x _iface _x ; do
306             [ -n "$_iface" ] || continue
307             
308             _table_id="${table_id_prefix}${_ip}"
309             if [ -z "$(ip route show table $_table_id 2>/dev/null)" -o \
310                 "$1" = "force" ]  ; then
311                 add_routing_for_ip "$_iface" "$_ip"
312             fi
313         done
314     } || exit $?
315 }
316
317 # Remove rules/routes for addresses that we're not hosting.  If a
318 # releaseip event failed in an earlier script then we might not have
319 # had a chance to remove the corresponding rules/routes.
320 remove_bogus_routes ()
321 {
322     # Get a IPs current hosted by this node, each anchored with '@'.
323     _ips=$($CTDB ip -v -X | awk -F'|' 'NR > 1 && $4 != "" {printf "@%s@\n", $2}')
324
325     ip rule show |
326     while read _p _x _i _x _t ; do
327         # Remove trailing colon after priority/preference.
328         _p="${_p%:}"
329         # Only remove rules that match our priority/preference.
330         [ "$CTDB_PER_IP_ROUTING_RULE_PREF" = "$_p" ] || continue
331         # Only remove rules for which we don't have an IP.  This could
332         # be done with grep, but let's do it with shell prefix removal
333         # to avoid unnecessary processes.  This falls through if
334         # "@${_i}@" isn't present in $_ips.
335         [ "$_ips" = "${_ips#*@${_i}@}" ] || continue
336
337         echo "Removing ip rule/routes for unhosted public address $_i"
338         del_routing_for_ip "$_i"
339     done
340 }
341
342 ######################################################################
343
344 service_reconfigure ()
345 {
346     add_missing_routes "force"
347     remove_bogus_routes
348
349     # flush our route cache
350     set_proc sys/net/ipv4/route/flush 1
351 }
352
353 ######################################################################
354
355 ctdb_check_args "$@"
356
357 ctdb_service_check_reconfigure
358
359 case "$1" in
360     startup)
361         flush_rules_and_routes
362
363         # make sure that we only respond to ARP messages from the NIC
364         # where a particular ip address is associated.
365         get_proc sys/net/ipv4/conf/all/arp_filter >/dev/null 2>&1 && {
366             set_proc sys/net/ipv4/conf/all/arp_filter 1
367         }
368         ;;
369
370     shutdown)
371         flush_rules_and_routes
372         clean_up_table_ids
373         ;;
374
375     takeip)
376         iface=$2
377         ip=$3
378         maskbits=$4
379
380         ensure_ipv4_is_valid_addr "$1" "$ip"
381         add_routing_for_ip "$iface" "$ip"
382
383         # flush our route cache
384         set_proc sys/net/ipv4/route/flush 1
385
386         $CTDB gratiousarp "$ip" "$iface"
387         ;;
388
389     updateip)
390         oiface=$2
391         niface=$3
392         ip=$4
393         maskbits=$5
394
395         ensure_ipv4_is_valid_addr "$1" "$ip"
396         add_routing_for_ip "$niface" "$ip"
397
398         # flush our route cache
399         set_proc sys/net/ipv4/route/flush 1
400
401         $CTDB gratiousarp "$ip" "$niface"
402         tickle_tcp_connections "$ip"
403         ;;
404
405     releaseip)
406         iface=$2
407         ip=$3
408         maskbits=$4
409
410         ensure_ipv4_is_valid_addr "$1" "$ip"
411         del_routing_for_ip "$ip"
412         ;;
413
414     ipreallocated)
415         add_missing_routes
416         remove_bogus_routes
417         ;;
418
419     *)
420         ctdb_standard_event_handler "$@"
421         ;;
422 esac
423
424 exit 0