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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
import argparse
import logging import logging
import re import re
import socket
import struct
from akanda.router import utils
from akanda.router.drivers import base from akanda.router.drivers import base
from akanda.router.models import Network
LOG = logging.getLogger(__name__) 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): class ARPManager(base.Manager):
""" """
A class to interact with entries in the ARP cache. Currently only really 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' 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): def remove_stale_entries(self, config):
""" """
A wrapper function that iterates over the networks in <config> and 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.sudo(*fmt_args_add(item))
self.up(interface) 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): for item in (prev_set - next_set):
self.sudo(*fmt_args_delete(item)) self.sudo(*fmt_args_delete(item))
ip, prefix = item ip, prefix = item

View File

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

View File

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

View File

@ -16,8 +16,10 @@
import mock import mock
import socket
import unittest2 import unittest2
from akanda.router import models
from akanda.router.drivers import arp from akanda.router.drivers import arp
config = mock.Mock() config = mock.Mock()
@ -26,6 +28,13 @@ alloc = mock.Mock()
network.address_allocations = [alloc] network.address_allocations = [alloc]
config.networks = [network] config.networks = [network]
def _AF_PACKET_supported():
try:
from socket import AF_PACKET
return True
except:
return False
class ARPTest(unittest2.TestCase): class ARPTest(unittest2.TestCase):
@ -83,3 +92,89 @@ class ARPTest(unittest2.TestCase):
mock.call('-an'), mock.call('-an'),
mock.call('-d', '10.10.10.2') 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'), ], 'sudo'),
mock.call([cmd, 'link', 'set', 'em0', 'up'], 'sudo'), mock.call([cmd, 'link', 'set', 'em0', 'up'], 'sudo'),
mock.call([cmd, 'addr', 'show', 'em0']), mock.call([cmd, 'addr', 'show', 'em0']),
mock.call([
'arping', '-A', '-c', '1', '-I', 'em0', '192.168.105.2'
], 'sudo'),
mock.call([ mock.call([
cmd, '-6', 'addr', 'add', cmd, '-6', 'addr', 'add',
'fdca:3ba5:a17a:acda:20c:29ff:fe94:723d/64', 'dev', 'em0' 'fdca:3ba5:a17a:acda:20c:29ff:fe94:723d/64', 'dev', 'em0'
@ -317,9 +314,6 @@ class IPTestCase(TestCase):
], 'sudo'), ], 'sudo'),
mock.call(['/sbin/ip', 'link', 'set', 'em0', 'up'], 'sudo'), mock.call(['/sbin/ip', 'link', 'set', 'em0', 'up'], 'sudo'),
mock.call(['/sbin/ip', 'addr', 'show', 'em0']), mock.call(['/sbin/ip', 'addr', 'show', 'em0']),
mock.call([
'arping', '-A', '-c', '1', '-I', 'em0', str(a.ip)
], 'sudo'),
mock.call([ mock.call([
'/sbin/ip', 'addr', 'del', str(c), 'dev', 'em0' '/sbin/ip', 'addr', 'del', str(c), 'dev', 'em0'
], 'sudo'), ], 'sudo'),