
This adds a new IP manager driver for configuring addresses and routes via keepalived instead of directly. It used when the logical resource is configured to be highly-available, according to configuration pushed by the orchestrator. We rely on a 'ha_resource' flag attached to the main config dict to enable it, and use specific HA config about peers and cluster priority contained in the 'ha_config' section of the main config. The resulting keepalived cluster contains a VRRP instance for each interface, with the exception of the management interface. Partially-implements: blueprint appliance-ha Change-Id: I5ababa41d65642b00f6b808197af9b2a59ebc67a
160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
# Copyright 2014 DreamHost, LLC
|
|
#
|
|
# Author: DreamHost, LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import argparse
|
|
import re
|
|
import socket
|
|
import struct
|
|
|
|
from astara_router import utils
|
|
from astara_router.drivers import base
|
|
from astara_router.models import Network
|
|
|
|
|
|
def send_gratuitous_arp():
|
|
parser = argparse.ArgumentParser(
|
|
description='Send a gratuitous ARP'
|
|
)
|
|
parser.add_argument('ifname', metavar='ifname', type=str)
|
|
parser.add_argument('address', metavar='address', type=str)
|
|
args = parser.parse_args()
|
|
|
|
return _send_gratuitous_arp(args.ifname, args.address)
|
|
|
|
|
|
def _send_gratuitous_arp(ifname, address):
|
|
"""
|
|
Send a gratuitous ARP reply. Generally used when Floating IPs are
|
|
associated.
|
|
:type ifname: str
|
|
:param ifname: The real name of the interface to send an ARP on
|
|
:type address: str
|
|
:param address: The source IPv4 address
|
|
"""
|
|
HTYPE_ARP = 0x0806
|
|
PTYPE_IPV4 = 0x0800
|
|
|
|
# Bind to the socket
|
|
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
|
|
sock.bind((ifname, HTYPE_ARP))
|
|
hwaddr = sock.getsockname()[4]
|
|
|
|
# Build a gratuitous ARP packet
|
|
gratuitous_arp = [
|
|
struct.pack("!h", 1), # HTYPE Ethernet
|
|
struct.pack("!h", PTYPE_IPV4), # PTYPE IPv4
|
|
struct.pack("!B", 6), # HADDR length, 6 for IEEE 802 MAC addresses
|
|
struct.pack("!B", 4), # PADDR length, 4 for IPv4
|
|
struct.pack("!h", 2), # OPER, 2 = ARP Reply
|
|
|
|
# Sender's hardware and protocol address are duplicated in the
|
|
# target fields
|
|
|
|
hwaddr, # Sender MAC
|
|
socket.inet_aton(address), # Sender IP address
|
|
hwaddr, # Target MAC
|
|
socket.inet_aton(address) # Target IP address
|
|
]
|
|
frame = [
|
|
'\xff\xff\xff\xff\xff\xff', # Broadcast destination
|
|
hwaddr, # Source address
|
|
struct.pack("!h", HTYPE_ARP),
|
|
''.join(gratuitous_arp)
|
|
]
|
|
sock.send(''.join(frame))
|
|
sock.close()
|
|
|
|
|
|
class ARPManager(base.Manager):
|
|
"""
|
|
A class to interact with entries in the ARP cache. Currently only really
|
|
provides support for deleting stuff from the cache.
|
|
"""
|
|
EXECUTABLE = 'arp'
|
|
|
|
def send_gratuitous_arp_for_floating_ips(self, config, generic_to_host):
|
|
"""
|
|
Send a gratuitous ARP for every Floating IP.
|
|
:type config: astara_router.models.Configuration
|
|
:param config: An astara_router.models.Configuration object containing
|
|
configuration information for the system's network
|
|
setup.
|
|
:type generic_to_host: callable
|
|
:param generic_to_host: A callable which translates a generic interface
|
|
name (e.g., "ge0") to a physical name (e.g.,
|
|
"eth0")
|
|
"""
|
|
external_nets = filter(
|
|
lambda n: n.network_type == Network.TYPE_EXTERNAL,
|
|
config.networks
|
|
)
|
|
for net in external_nets:
|
|
for fip in net.floating_ips:
|
|
utils.execute([
|
|
'astara-gratuitous-arp',
|
|
generic_to_host(net.interface.ifname),
|
|
str(fip.floating_ip)
|
|
], self.root_helper)
|
|
|
|
def remove_stale_entries(self, config):
|
|
"""
|
|
A wrapper function that iterates over the networks in <config> and
|
|
removes arp entries that no longer have any networks associated with
|
|
them. This function calls _delete_from_arp_cache to do the actual
|
|
deletion and makes calls to _mac_address_for_ip to match arp entries
|
|
to network interface IPs.
|
|
|
|
:type config: astara_router.models.Configuration
|
|
:param config: An astara_router.models.Configuration object containing
|
|
configuration information for the system's network
|
|
setup.
|
|
"""
|
|
for network in config.networks:
|
|
for a in network.address_allocations:
|
|
for ip in a.dhcp_addresses:
|
|
address_for_ip = self._mac_address_for_ip(ip)
|
|
if address_for_ip and address_for_ip != a.mac_address:
|
|
self._delete_from_arp_cache(ip)
|
|
|
|
def _mac_address_for_ip(self, ip):
|
|
"""
|
|
Matches a network's IP address to an arp entry. This is used to
|
|
associate arp entries with networks that are configured on the system
|
|
and to determine which arp entries are stale through process of
|
|
elemination.
|
|
|
|
:type ip: str
|
|
:param ip: IP address to search for in the ARP table.
|
|
"""
|
|
cmd_out = self.sudo('-an')
|
|
match = re.search(' \(%s\) at ([^\s]+)' % ip, cmd_out)
|
|
if match and match.groups():
|
|
return match.group(1)
|
|
|
|
def _delete_from_arp_cache(self, ip):
|
|
"""
|
|
Runs `arp -d <ip>` to delete <ip> from the arp cache.
|
|
|
|
:type ip: str
|
|
:param ip: IP address to search for in the ARP table.
|
|
"""
|
|
try:
|
|
self.sudo('-d', ip)
|
|
except:
|
|
# We may be attempting to delete from ARP for interfaces which
|
|
# are managed by keepalived and do not yet have addresses
|
|
pass
|