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