Remove the arping dependency and send gratuitous ARP via Python's socket lib.
Change-Id: Ib9f4f0e9165c10b5ae5ff9e26ae79c1c335489cc
This commit is contained in:
parent
aa72fd46b5
commit
21838623b3
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
])
|
||||
|
|
|
@ -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'),
|
||||
|
|
Loading…
Reference in New Issue