Remove the arping dependency and send gratuitous ARP via Python's socket lib.

Change-Id: Ib9f4f0e9165c10b5ae5ff9e26ae79c1c335489cc
This commit is contained in:
Ryan Petrello 2015-06-05 11:19:07 -04:00
parent aa72fd46b5
commit 21838623b3
6 changed files with 183 additions and 14 deletions

View File

@ -14,16 +14,74 @@
# License for the specific language governing permissions and limitations
# under the License.
import argparse
import logging
import re
import socket
import struct
from akanda.router import utils
from akanda.router.drivers import base
from akanda.router.models import Network
LOG = logging.getLogger(__name__)
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
@ -31,6 +89,30 @@ class ARPManager(base.Manager):
"""
EXECUTABLE = '/usr/sbin/arp'
def send_gratuitous_arp_for_floating_ips(self, config, generic_to_host):
"""
Send a gratuitous ARP for every Floating IP.
:type config: akanda.router.models.Configuration
:param config: An akanda.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([
'akanda-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

View File

@ -228,13 +228,6 @@ class IPManager(base.Manager):
self.sudo(*fmt_args_add(item))
self.up(interface)
# Send a gratuitous ARP for new v4 addressees
ip, prefix = item
if ip.version == 4:
utils.execute([
'arping', '-A', '-c', '1', '-I', real_ifname, str(ip)
], self.root_helper)
for item in (prev_set - next_set):
self.sudo(*fmt_args_delete(item))
ip, prefix = item

View File

@ -101,6 +101,10 @@ class Manager(object):
def update_arp(self):
mgr = arp.ARPManager()
mgr.send_gratuitous_arp_for_floating_ips(
self.config,
self.ip_mgr.generic_to_host
)
mgr.remove_stale_entries(self.config)
def get_interfaces(self):

View File

@ -35,6 +35,7 @@ console_scripts =
akanda-configure-management=akanda.router.commands.management:configure_management
akanda-api-dev-server=akanda.router.api.server:main
akanda-metadata-proxy=akanda.router.metadata_proxy:main
akanda-gratuitous-arp=akanda.router.drivers.arp:send_gratuitous_arp
[build_sphinx]
all_files = 1

View File

@ -16,8 +16,10 @@
import mock
import socket
import unittest2
from akanda.router import models
from akanda.router.drivers import arp
config = mock.Mock()
@ -26,6 +28,13 @@ alloc = mock.Mock()
network.address_allocations = [alloc]
config.networks = [network]
def _AF_PACKET_supported():
try:
from socket import AF_PACKET
return True
except:
return False
class ARPTest(unittest2.TestCase):
@ -83,3 +92,89 @@ class ARPTest(unittest2.TestCase):
mock.call('-an'),
mock.call('-d', '10.10.10.2')
])
def test_send_gratuitous_arp_for_config(self):
config = models.Configuration({
'networks': [{
'network_id': 'ABC456',
'interface': {
'ifname': 'ge1',
'name': 'ext',
},
'subnets': [{
'cidr': '172.16.77.0/24',
'gateway_ip': '172.16.77.1',
'dhcp_enabled': True,
'dns_nameservers': []
}],
'network_type': models.Network.TYPE_EXTERNAL,
}],
'floating_ips': [{
'fixed_ip': '192.168.0.2',
'floating_ip': '172.16.77.50'
},{
'fixed_ip': '192.168.0.3',
'floating_ip': '172.16.77.51'
},{
'fixed_ip': '192.168.0.4',
'floating_ip': '172.16.77.52'
},{
'fixed_ip': '192.168.0.5',
'floating_ip': '172.16.77.53'
}]
})
with mock.patch('akanda.router.utils.execute') as execute:
self.mgr.send_gratuitous_arp_for_floating_ips(
config,
lambda x: x.replace('ge', 'eth')
)
assert execute.call_args_list == [
mock.call(
['akanda-gratuitous-arp', 'eth1', '172.16.77.50'], 'sudo'
),
mock.call(
['akanda-gratuitous-arp', 'eth1', '172.16.77.51'], 'sudo'
),
mock.call(
['akanda-gratuitous-arp', 'eth1', '172.16.77.52'], 'sudo'
),
mock.call(
['akanda-gratuitous-arp', 'eth1', '172.16.77.53'], 'sudo'
)
]
@unittest2.skipIf(
not _AF_PACKET_supported(),
'socket.AF_PACKET not supported on this platform'
)
@mock.patch('socket.socket')
def test_send_gratuitous_arp(self, socket_constr):
socket_inst = socket_constr.return_value
socket_inst.getsockname.return_value = (
None, None, None, None, 'A1:B2:C3:D4:E5:F6'
)
arp._send_gratuitous_arp('eth1', '1.2.3.4')
socket_constr.assert_called_once_with(
socket.AF_PACKET, socket.SOCK_RAW
)
socket_inst.bind.assert_called_once_with((
'eth1',
0x0806
))
data = socket_inst.send.call_args_list[0][0][0]
assert data == ''.join([
'\xff\xff\xff\xff\xff\xff', # Broadcast destination
'A1:B2:C3:D4:E5:F6', # Source hardware address
'\x08\x06', # HTYPE ARP
'\x00\x01', # Ethernet
'\x08\x00', # Protocol IPv4
'\x06', # HADDR length, 6 for IEEE 802 MAC addresses
'\x04', # PADDR length, 4 for IPv4
'\x00\x02', # OPER, 2 = ARP Reply
'A1:B2:C3:D4:E5:F6', # Source MAC
'\x01\x02\x03\x04', # Source IP
'A1:B2:C3:D4:E5:F6', # Target MAC matches
'\x01\x02\x03\x04' # Target IP matches
])

View File

@ -259,9 +259,6 @@ class IPTestCase(TestCase):
], 'sudo'),
mock.call([cmd, 'link', 'set', 'em0', 'up'], 'sudo'),
mock.call([cmd, 'addr', 'show', 'em0']),
mock.call([
'arping', '-A', '-c', '1', '-I', 'em0', '192.168.105.2'
], 'sudo'),
mock.call([
cmd, '-6', 'addr', 'add',
'fdca:3ba5:a17a:acda:20c:29ff:fe94:723d/64', 'dev', 'em0'
@ -317,9 +314,6 @@ class IPTestCase(TestCase):
], 'sudo'),
mock.call(['/sbin/ip', 'link', 'set', 'em0', 'up'], 'sudo'),
mock.call(['/sbin/ip', 'addr', 'show', 'em0']),
mock.call([
'arping', '-A', '-c', '1', '-I', 'em0', str(a.ip)
], 'sudo'),
mock.call([
'/sbin/ip', 'addr', 'del', str(c), 'dev', 'em0'
], 'sudo'),