Merge "Add the base class DHCPResponder"
This commit is contained in:
commit
b526f2e680
|
@ -0,0 +1,181 @@
|
|||
# Copyright (c) 2021 China Unicom Cloud Data Co.,Ltd.
|
||||
# Copyright (c) 2019 - 2020 China Telecom Corporation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 math
|
||||
import struct
|
||||
|
||||
import netaddr
|
||||
from os_ken.lib import addrconv
|
||||
from os_ken.lib.packet import dhcp
|
||||
from os_ken.lib.packet import dhcp6
|
||||
from os_ken.lib.packet import ethernet
|
||||
from os_ken.lib.packet import ipv4
|
||||
from os_ken.lib.packet import ipv6
|
||||
from os_ken.lib.packet import packet
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.native \
|
||||
import base_oskenapp
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
IPV4_STR = "v4"
|
||||
IPV6_STR = "v6"
|
||||
|
||||
|
||||
class DHCPResponderBase(base_oskenapp.BaseNeutronAgentOSKenApp):
|
||||
|
||||
def __init__(self, agent_api, ext_api, version=IPV4_STR, *args, **kwargs):
|
||||
super(DHCPResponderBase, self).__init__(*args, **kwargs)
|
||||
self.agent_api = agent_api
|
||||
self.int_br = self.agent_api.request_int_br()
|
||||
self.ext_api = ext_api
|
||||
self.version = version
|
||||
self.name = "DHCP%sResponder" % version
|
||||
|
||||
self.hw_addr = cfg.CONF.base_mac
|
||||
self.register_packet_in_handler(self._packet_in_handler)
|
||||
|
||||
def _packet_in_handler(self, event):
|
||||
msg = event.msg
|
||||
datapath = msg.datapath
|
||||
ofproto = datapath.ofproto
|
||||
|
||||
if msg.reason != ofproto.OFPR_ACTION:
|
||||
LOG.debug("DHCP Controller only handle the packet which "
|
||||
"match the rules and the action is send to the "
|
||||
"controller.")
|
||||
return
|
||||
|
||||
of_in_port = msg.match['in_port']
|
||||
LOG.info("DHCP Controller packet in OF port: %s", of_in_port)
|
||||
pkt = packet.Packet(data=msg.data)
|
||||
|
||||
LOG.debug('DHCP Controller packet received: '
|
||||
'buffer_id=%x total_len=%d reason=ACTION '
|
||||
'table_id=%d cookie=%d match=%s pkt=%s',
|
||||
msg.buffer_id, msg.total_len,
|
||||
msg.table_id, msg.cookie, msg.match,
|
||||
pkt)
|
||||
|
||||
if self.version == IPV4_STR:
|
||||
ip_protocol = ipv4.ipv4
|
||||
dhcp_protocol = dhcp.dhcp
|
||||
else:
|
||||
ip_protocol = ipv6.ipv6
|
||||
dhcp_protocol = dhcp6.dhcp6
|
||||
ip_header = pkt.get_protocol(ip_protocol)
|
||||
if not ip_header:
|
||||
LOG.warning("DHCP Controller received packet "
|
||||
"is not an IP%s packet",
|
||||
self.version)
|
||||
return
|
||||
|
||||
dhcp_pkt = pkt.get_protocol(dhcp_protocol)
|
||||
if not dhcp_pkt:
|
||||
LOG.warning("DHCP Controller received packet "
|
||||
"is not a DHCP%s packet",
|
||||
self.version)
|
||||
return
|
||||
|
||||
eth_pkt = pkt.get_protocol(ethernet.ethernet)
|
||||
port_id = self.get_port_id_from_br(of_in_port, eth_pkt.src)
|
||||
LOG.debug("DHCP Controller received DHCP%s packet's neutron port "
|
||||
"id: %s", self.version, port_id)
|
||||
|
||||
port_info = self.ext_api.get_port_info(port_id)
|
||||
LOG.debug("DHCP Controller received DHCP%s packet's neutron port "
|
||||
"info: %s", self.version, port_info)
|
||||
if not port_info:
|
||||
return
|
||||
|
||||
self.handle_dhcp(datapath, of_in_port, pkt, port_info)
|
||||
|
||||
def get_bin_dns(self, dns_nameservers):
|
||||
if self.version == IPV4_STR:
|
||||
text_protocol = addrconv.ipv4
|
||||
else:
|
||||
text_protocol = addrconv.ipv6
|
||||
|
||||
dns_bin = b''
|
||||
for dns in dns_nameservers:
|
||||
dns_bin += text_protocol.text_to_bin(dns['address'])
|
||||
return dns_bin
|
||||
|
||||
def get_bin_route(self, destination, nexthop):
|
||||
if self.version == IPV4_STR:
|
||||
text_protocol = addrconv.ipv4
|
||||
else:
|
||||
text_protocol = addrconv.ipv6
|
||||
|
||||
bin_route = b''
|
||||
net = netaddr.IPNetwork(str(destination))
|
||||
dest = str(net.ip)
|
||||
mask = net.prefixlen
|
||||
bin_route += struct.pack('B', mask)
|
||||
bin_addr = text_protocol.text_to_bin(dest)
|
||||
dest_len = int(math.ceil(mask / 8.0))
|
||||
bin_route += bin_addr[:dest_len]
|
||||
bin_route += text_protocol.text_to_bin(nexthop)
|
||||
return bin_route
|
||||
|
||||
def get_port_id_from_br(self, ofport, vif_mac):
|
||||
vifs = self.int_br.get_vif_ports()
|
||||
for vif in vifs:
|
||||
if vif.ofport == ofport and vif.vif_mac == vif_mac:
|
||||
return vif.vif_id
|
||||
|
||||
def get_port_ip(self, port_info, ip_version):
|
||||
fixed_ips = port_info['fixed_ips']
|
||||
for ip in fixed_ips:
|
||||
ipaddr = netaddr.IPNetwork(ip['ip_address'])
|
||||
# For the first IP only, secondary IPs will not be returned.
|
||||
if ipaddr.version == ip_version:
|
||||
return ip
|
||||
|
||||
def get_dhcp_options(self, port_info, is_ack):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_ret_packet(self, packet_in, port_info, is_ack=False):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_state(self, pkt_dhcp):
|
||||
raise NotImplementedError()
|
||||
|
||||
def handle_dhcp(self, datapath, ofport, pkt, port_info):
|
||||
raise NotImplementedError()
|
||||
|
||||
def packet_out(self, datapath, ofport, pkt):
|
||||
if not pkt:
|
||||
LOG.debug("DHCP Controller no packet assembled for DHCP%s.",
|
||||
self.version)
|
||||
return
|
||||
ofproto = datapath.ofproto
|
||||
parser = datapath.ofproto_parser
|
||||
pkt.serialize()
|
||||
LOG.debug("DHCP Controller packet assembled for DHCP%s %s",
|
||||
self.version, (pkt,))
|
||||
data = pkt.data
|
||||
actions = [parser.OFPActionOutput(port=ofport)]
|
||||
out = parser.OFPPacketOut(datapath=datapath,
|
||||
buffer_id=ofproto.OFP_NO_BUFFER,
|
||||
in_port=ofproto.OFPP_CONTROLLER,
|
||||
actions=actions,
|
||||
data=data)
|
||||
LOG.info("DHCP Controller assembled DHCP%s packet out to OF port %s",
|
||||
self.version, ofport)
|
||||
datapath.send_msg(out)
|
|
@ -0,0 +1,206 @@
|
|||
# Copyright (c) 2021 China Unicom Cloud Data Co.,Ltd.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import netaddr
|
||||
from neutron_lib import constants
|
||||
from neutron_lib.utils import net as net_utils
|
||||
from os_ken.lib import addrconv
|
||||
from os_ken.lib.packet import dhcp
|
||||
from os_ken.lib.packet import ethernet
|
||||
from os_ken.lib.packet import in_proto as inet
|
||||
from os_ken.lib.packet import ipv4
|
||||
from os_ken.lib.packet import packet
|
||||
from os_ken.lib.packet import udp
|
||||
|
||||
from neutron.agent.common import ovs_lib
|
||||
from neutron.agent.l2.extensions.dhcp import base as dhcp_resp_base
|
||||
from neutron.objects import subnet as subnet_obj
|
||||
from neutron.plugins.ml2.drivers.openvswitch.agent \
|
||||
import ovs_agent_extension_api as ovs_ext_api
|
||||
from neutron.tests import base
|
||||
|
||||
|
||||
class FakeOF(object):
|
||||
OFPR_NO_MATCH = 0
|
||||
OFPR_ACTION = 1
|
||||
FPR_INVALID_TTL = 2
|
||||
OFP_NO_BUFFER = 3
|
||||
OFPP_CONTROLLER = 4
|
||||
|
||||
|
||||
class FakeDatapath(object):
|
||||
ofproto = FakeOF()
|
||||
|
||||
|
||||
class FakeMsg(object):
|
||||
datapath = FakeDatapath()
|
||||
reason = datapath.ofproto.OFPR_ACTION
|
||||
match = {'in_port': 1}
|
||||
data = ""
|
||||
buffer_id = 1
|
||||
total_len = 1
|
||||
table_id = 77
|
||||
cookie = 1
|
||||
|
||||
def set_data(self, packet):
|
||||
packet.serialize()
|
||||
self.data = packet.data
|
||||
|
||||
|
||||
PORT_INFO = {
|
||||
'device_owner': 'compute:nova',
|
||||
'admin_state_up': True,
|
||||
'network_id': 'd666ccb3-69e9-46cb-b157-bb3741d87d5a',
|
||||
'fixed_ips': [
|
||||
{'version': 4,
|
||||
'host_routes': [
|
||||
subnet_obj.Route(
|
||||
destination=netaddr.IPNetwork('1.1.1.0/24'),
|
||||
nexthop='192.168.1.100',
|
||||
subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f'),
|
||||
subnet_obj.Route(
|
||||
destination=netaddr.IPNetwork('2.2.2.2/32'),
|
||||
nexthop='192.168.1.101',
|
||||
subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f')],
|
||||
'subnet_id': 'daed3c3d-d95a-48a8-a8b1-17d408cd760f',
|
||||
'dns_nameservers': [
|
||||
subnet_obj.DNSNameServer(
|
||||
address='8.8.8.8',
|
||||
order=0,
|
||||
subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f'),
|
||||
subnet_obj.DNSNameServer(
|
||||
address='8.8.4.4',
|
||||
order=1,
|
||||
subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f')],
|
||||
'cidr': net_utils.AuthenticIPNetwork('192.168.111.0/24'),
|
||||
'ip_address': '192.168.111.45',
|
||||
'gateway_ip': netaddr.IPAddress('192.168.111.1')},
|
||||
{'version': 6,
|
||||
'host_routes': [],
|
||||
'subnet_id': 'bd013460-b05f-4927-a4c6-5127584b2487',
|
||||
'dns_nameservers': [],
|
||||
'cidr': net_utils.AuthenticIPNetwork('fda7:a5cc:3460:1::/64'),
|
||||
'ip_address': 'fda7:a5cc:3460:1::bf',
|
||||
'gateway_ip': netaddr.IPAddress('fda7:a5cc:3460:1::1')}
|
||||
],
|
||||
'mac_address': '00:01:02:03:04:05',
|
||||
'port_id': '9a0e1889-f05f-43c7-a319-e1a723ed1587',
|
||||
'mtu': 1450
|
||||
}
|
||||
|
||||
|
||||
class DHCPResponderBaseTestCase(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DHCPResponderBaseTestCase, self).setUp()
|
||||
self.int_br = mock.Mock()
|
||||
self.tun_br = mock.Mock()
|
||||
self.agent_api = ovs_ext_api.OVSAgentExtensionAPI(
|
||||
self.int_br,
|
||||
self.tun_br,
|
||||
phys_brs=None)
|
||||
self.ext_api = mock.Mock()
|
||||
self.base_responer = dhcp_resp_base.DHCPResponderBase(self.agent_api,
|
||||
self.ext_api)
|
||||
self.base_responer.int_br = self.int_br
|
||||
|
||||
self.base_responer.get_dhcp_options = mock.Mock()
|
||||
self.base_responer.get_ret_packet = mock.Mock()
|
||||
self.base_responer.get_state = mock.Mock()
|
||||
self.base_responer.handle_dhcp = mock.Mock()
|
||||
|
||||
self.port_info = PORT_INFO
|
||||
|
||||
def _create_test_dhcp_request_packet(self):
|
||||
option_list = []
|
||||
bin_server = addrconv.ipv4.text_to_bin('192.168.1.1')
|
||||
option_list.append(
|
||||
dhcp.option(tag=dhcp.DHCP_SERVER_IDENTIFIER_OPT,
|
||||
value=bin_server))
|
||||
option_list.append(
|
||||
dhcp.option(tag=dhcp.DHCP_MESSAGE_TYPE_OPT,
|
||||
value=b'\x03'))
|
||||
options = dhcp.options(option_list=option_list)
|
||||
ret_pkt = packet.Packet()
|
||||
ret_pkt.add_protocol(ethernet.ethernet(
|
||||
dst="ff:ff:ff:ff:ff:ff",
|
||||
src=self.port_info['mac_address']))
|
||||
ret_pkt.add_protocol(
|
||||
ipv4.ipv4(dst="255.255.255.255",
|
||||
src="0.0.0.0",
|
||||
proto=inet.IPPROTO_UDP))
|
||||
ret_pkt.add_protocol(udp.udp(
|
||||
src_port=constants.DHCP_CLIENT_PORT,
|
||||
dst_port=constants.DHCP_RESPONSE_PORT))
|
||||
ret_pkt.add_protocol(dhcp.dhcp(op=dhcp.DHCP_BOOT_REQUEST,
|
||||
chaddr=self.port_info['mac_address'],
|
||||
siaddr='0.0.0.0',
|
||||
xid=3454038351,
|
||||
options=options))
|
||||
return ret_pkt
|
||||
|
||||
def test__packet_in_handler(self):
|
||||
ev = mock.Mock()
|
||||
ev.msg = FakeMsg()
|
||||
dhcp_req = self._create_test_dhcp_request_packet()
|
||||
ev.msg.set_data(dhcp_req)
|
||||
|
||||
self.base_responer.get_port_id_from_br = mock.Mock()
|
||||
self.base_responer.get_port_id_from_br.return_value = (
|
||||
PORT_INFO['port_id'])
|
||||
self.ext_api.get_port_info = mock.Mock()
|
||||
self.ext_api.get_port_info.return_value = PORT_INFO
|
||||
self.base_responer._packet_in_handler(ev)
|
||||
self.base_responer.handle_dhcp.assert_called_once_with(
|
||||
ev.msg.datapath, 1, mock.ANY, self.port_info)
|
||||
|
||||
def test_get_bin_dns(self):
|
||||
except_value = b'\x08\x08\x08\x08\x08\x08\x04\x04'
|
||||
bin_dns = self.base_responer.get_bin_dns(
|
||||
self.port_info['fixed_ips'][0]['dns_nameservers'])
|
||||
self.assertEqual(except_value, bin_dns)
|
||||
|
||||
def test_get_bin_route(self):
|
||||
expect_bin_route = b' \x08\x08\x08\x08\xc0\xa8\x01\x01'
|
||||
bin_route = self.base_responer.get_bin_route('8.8.8.8', '192.168.1.1')
|
||||
self.assertEqual(expect_bin_route, bin_route)
|
||||
|
||||
def test_get_port_id_from_br(self):
|
||||
self.int_br.get_vif_ports = mock.Mock()
|
||||
self.int_br.get_vif_ports.return_value = [
|
||||
ovs_lib.VifPort(port_name="tap-1", ofport=1,
|
||||
vif_id=PORT_INFO['port_id'],
|
||||
vif_mac=PORT_INFO['mac_address'],
|
||||
switch='br-int')]
|
||||
pid = self.base_responer.get_port_id_from_br(
|
||||
1, PORT_INFO['mac_address'])
|
||||
self.assertEqual(PORT_INFO['port_id'], pid)
|
||||
|
||||
def test_get_port_ip(self):
|
||||
ip_v4 = self.base_responer.get_port_ip(self.port_info, 4)
|
||||
self.assertEqual(self.port_info['fixed_ips'][0],
|
||||
ip_v4)
|
||||
ip_v6 = self.base_responer.get_port_ip(self.port_info, 6)
|
||||
self.assertEqual(self.port_info['fixed_ips'][1],
|
||||
ip_v6)
|
||||
|
||||
def test_packet_out(self):
|
||||
datapath = mock.Mock()
|
||||
ofport = 1
|
||||
dhcp_req = self._create_test_dhcp_request_packet()
|
||||
self.base_responer.packet_out(datapath, ofport, dhcp_req)
|
||||
datapath.send_msg.assert_called_once_with(mock.ANY)
|
Loading…
Reference in New Issue