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
|
# 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
])
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
Loading…
Reference in New Issue