433 lines
17 KiB
Python
433 lines
17 KiB
Python
# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
|
|
#
|
|
# 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 oslo_config import cfg
|
|
from oslo_log import helpers as log_helpers
|
|
from oslo_log import log as logging
|
|
|
|
from neutron.agent.common import utils as common_utils
|
|
from neutron.agent.l3 import dvr_snat_ns
|
|
from neutron.agent.l3 import namespaces
|
|
from neutron.agent.linux import ip_lib
|
|
from neutron.agent.linux import iptables_manager
|
|
from neutron.common import constants
|
|
from neutron.common import ipv6_utils
|
|
from neutron.conf.agent import common as config
|
|
from neutron.services.metering.drivers import abstract_driver
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
NS_PREFIX = 'qrouter-'
|
|
WRAP_NAME = 'neutron-meter'
|
|
EXTERNAL_DEV_PREFIX = 'qg-'
|
|
ROUTER_2_FIP_DEV_PREFIX = namespaces.ROUTER_2_FIP_DEV_PREFIX
|
|
TOP_CHAIN = WRAP_NAME + "-FORWARD"
|
|
RULE = '-r-'
|
|
LABEL = '-l-'
|
|
|
|
config.register_interface_driver_opts_helper(cfg.CONF)
|
|
config.register_interface_opts()
|
|
|
|
|
|
class IptablesManagerTransaction(object):
|
|
__transactions = {}
|
|
|
|
def __init__(self, im):
|
|
self.im = im
|
|
|
|
transaction = self.__transactions.get(im, 0)
|
|
transaction += 1
|
|
self.__transactions[im] = transaction
|
|
|
|
def __enter__(self):
|
|
return self.im
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
transaction = self.__transactions.get(self.im)
|
|
if transaction == 1:
|
|
self.im.apply()
|
|
del self.__transactions[self.im]
|
|
else:
|
|
transaction -= 1
|
|
self.__transactions[self.im] = transaction
|
|
|
|
|
|
class RouterWithMetering(object):
|
|
|
|
def __init__(self, conf, router):
|
|
self.conf = conf
|
|
self.id = router['id']
|
|
self.router = router
|
|
# TODO(cbrandily): deduplicate ns_name generation in metering/l3
|
|
self.ns_name = NS_PREFIX + self.id
|
|
self.iptables_manager = None
|
|
self.snat_iptables_manager = None
|
|
if self.router['distributed']:
|
|
# If distributed routers then we need to apply the
|
|
# metering agent label rules in the snat namespace as well.
|
|
snat_ns_name = dvr_snat_ns.SnatNamespace.get_snat_ns_name(
|
|
self.id)
|
|
# Check for namespace existence before we assign the
|
|
# snat_iptables_manager
|
|
if ip_lib.network_namespace_exists(snat_ns_name):
|
|
self.snat_iptables_manager = iptables_manager.IptablesManager(
|
|
namespace=snat_ns_name,
|
|
binary_name=WRAP_NAME,
|
|
state_less=True,
|
|
use_ipv6=ipv6_utils.is_enabled_and_bind_by_default())
|
|
# Check of namespace existence before we assign the iptables_manager
|
|
# NOTE(Swami): If distributed routers, all external traffic on a
|
|
# compute node will flow through the rfp interface in the router
|
|
# namespace.
|
|
if ip_lib.network_namespace_exists(self.ns_name):
|
|
self.iptables_manager = iptables_manager.IptablesManager(
|
|
namespace=self.ns_name,
|
|
binary_name=WRAP_NAME,
|
|
state_less=True,
|
|
use_ipv6=ipv6_utils.is_enabled_and_bind_by_default())
|
|
self.metering_labels = {}
|
|
|
|
|
|
class IptablesMeteringDriver(abstract_driver.MeteringAbstractDriver):
|
|
|
|
def __init__(self, plugin, conf):
|
|
self.plugin = plugin
|
|
self.conf = conf or cfg.CONF
|
|
self.routers = {}
|
|
|
|
self.driver = common_utils.load_interface_driver(self.conf)
|
|
|
|
def _update_router(self, router):
|
|
r = self.routers.get(router['id'],
|
|
RouterWithMetering(self.conf, router))
|
|
r.router = router
|
|
self.routers[r.id] = r
|
|
|
|
return r
|
|
|
|
@log_helpers.log_method_call
|
|
def update_routers(self, context, routers):
|
|
# disassociate removed routers
|
|
router_ids = set(router['id'] for router in routers)
|
|
for router_id, rm in self.routers.items():
|
|
if router_id not in router_ids:
|
|
self._process_disassociate_metering_label(rm.router)
|
|
|
|
for router in routers:
|
|
old_gw_port_id = None
|
|
old_rm = self.routers.get(router['id'])
|
|
if old_rm:
|
|
old_gw_port_id = old_rm.router['gw_port_id']
|
|
gw_port_id = router['gw_port_id']
|
|
|
|
if gw_port_id != old_gw_port_id:
|
|
if old_rm:
|
|
if router.get('distributed'):
|
|
old_rm_im = old_rm.snat_iptables_manager
|
|
else:
|
|
old_rm_im = old_rm.iptables_manager
|
|
|
|
with IptablesManagerTransaction(old_rm_im):
|
|
self._process_disassociate_metering_label(router)
|
|
if gw_port_id:
|
|
self._process_associate_metering_label(router)
|
|
elif gw_port_id:
|
|
self._process_associate_metering_label(router)
|
|
|
|
@log_helpers.log_method_call
|
|
def remove_router(self, context, router_id):
|
|
if router_id in self.routers:
|
|
del self.routers[router_id]
|
|
|
|
def get_external_device_names(self, rm):
|
|
gw_port_id = rm.router.get('gw_port_id')
|
|
if not gw_port_id:
|
|
return None, None
|
|
|
|
# NOTE (Swami): External device 'qg' should be used on the
|
|
# Router namespace if the router is legacy and should be used on
|
|
# SNAT namespace if the router is distributed.
|
|
ext_dev = (EXTERNAL_DEV_PREFIX +
|
|
gw_port_id)[:self.driver.DEV_NAME_LEN]
|
|
ext_snat_dev = (ROUTER_2_FIP_DEV_PREFIX +
|
|
rm.id)[:self.driver.DEV_NAME_LEN]
|
|
return ext_dev, ext_snat_dev
|
|
|
|
def _process_metering_label_rules(self, rules, label_chain,
|
|
rules_chain, ext_dev, im):
|
|
if not ext_dev:
|
|
return
|
|
for rule in rules:
|
|
self._add_rule_to_chain(ext_dev, rule, im,
|
|
label_chain, rules_chain)
|
|
|
|
def _process_metering_label_rule_add(self, rule, ext_dev,
|
|
label_chain, rules_chain, im):
|
|
self._add_rule_to_chain(ext_dev, rule, im, label_chain, rules_chain)
|
|
|
|
def _process_metering_label_rule_delete(self, rule, ext_dev,
|
|
label_chain, rules_chain, im):
|
|
self._remove_rule_from_chain(ext_dev, rule, im,
|
|
label_chain, rules_chain)
|
|
|
|
def _add_rule_to_chain(self, ext_dev, rule, im,
|
|
label_chain, rules_chain):
|
|
ipt_rule = self._prepare_rule(ext_dev, rule, label_chain)
|
|
if rule['excluded']:
|
|
im.ipv4['filter'].add_rule(rules_chain, ipt_rule,
|
|
wrap=False, top=True)
|
|
else:
|
|
im.ipv4['filter'].add_rule(rules_chain, ipt_rule,
|
|
wrap=False, top=False)
|
|
|
|
def _remove_rule_from_chain(self, ext_dev, rule, im,
|
|
label_chain, rules_chain):
|
|
ipt_rule = self._prepare_rule(ext_dev, rule, label_chain)
|
|
if rule['excluded']:
|
|
im.ipv4['filter'].remove_rule(rules_chain, ipt_rule,
|
|
wrap=False, top=True)
|
|
else:
|
|
im.ipv4['filter'].remove_rule(rules_chain, ipt_rule,
|
|
wrap=False, top=False)
|
|
|
|
def _prepare_rule(self, ext_dev, rule, label_chain):
|
|
remote_ip = rule['remote_ip_prefix']
|
|
if rule['direction'] == 'egress':
|
|
dir_opt = '-s %s -o %s' % (remote_ip, ext_dev)
|
|
else:
|
|
dir_opt = '-d %s -i %s' % (remote_ip, ext_dev)
|
|
|
|
if rule['excluded']:
|
|
ipt_rule = '%s -j RETURN' % dir_opt
|
|
else:
|
|
ipt_rule = '%s -j %s' % (dir_opt, label_chain)
|
|
return ipt_rule
|
|
|
|
def _process_ns_specific_metering_label(self, router, ext_dev, im):
|
|
'''Process metering label based on the associated namespaces.'''
|
|
rm = self.routers.get(router['id'])
|
|
with IptablesManagerTransaction(im):
|
|
labels = router.get(constants.METERING_LABEL_KEY, [])
|
|
for label in labels:
|
|
label_id = label['id']
|
|
|
|
label_chain = iptables_manager.get_chain_name(
|
|
WRAP_NAME + LABEL + label_id, wrap=False)
|
|
|
|
rules_chain = iptables_manager.get_chain_name(
|
|
WRAP_NAME + RULE + label_id, wrap=False)
|
|
|
|
exists = rm.metering_labels.get(label_id)
|
|
if not exists:
|
|
self._create_metering_label_chain(rm,
|
|
label_chain,
|
|
rules_chain)
|
|
rm.metering_labels[label_id] = label
|
|
|
|
rules = label.get('rules')
|
|
if rules:
|
|
self._process_metering_label_rules(
|
|
rules, label_chain, rules_chain, ext_dev, im)
|
|
|
|
def _process_associate_metering_label(self, router):
|
|
self._update_router(router)
|
|
rm = self.routers.get(router['id'])
|
|
|
|
ext_dev, ext_snat_dev = self.get_external_device_names(rm)
|
|
for (im, dev) in [(rm.iptables_manager, ext_dev),
|
|
(rm.snat_iptables_manager, ext_snat_dev)]:
|
|
if im:
|
|
self._process_ns_specific_metering_label(router, dev, im)
|
|
|
|
def _process_ns_specific_disassociate_metering_label(self, router, im):
|
|
'''Disassociate metering label based on specific namespaces.'''
|
|
rm = self.routers.get(router['id'])
|
|
with IptablesManagerTransaction(im):
|
|
labels = router.get(constants.METERING_LABEL_KEY, [])
|
|
for label in labels:
|
|
label_id = label['id']
|
|
if label_id not in rm.metering_labels:
|
|
continue
|
|
|
|
label_chain = iptables_manager.get_chain_name(
|
|
WRAP_NAME + LABEL + label_id, wrap=False)
|
|
rules_chain = iptables_manager.get_chain_name(
|
|
WRAP_NAME + RULE + label_id, wrap=False)
|
|
im.ipv4['filter'].remove_chain(label_chain, wrap=False)
|
|
im.ipv4['filter'].remove_chain(rules_chain, wrap=False)
|
|
|
|
def _process_disassociate_metering_label(self, router):
|
|
rm = self.routers.get(router['id'])
|
|
if not rm:
|
|
return
|
|
|
|
for im in [rm.iptables_manager, rm.snat_iptables_manager]:
|
|
if im:
|
|
self._process_ns_specific_disassociate_metering_label(
|
|
router, im)
|
|
|
|
labels = router.get(constants.METERING_LABEL_KEY, [])
|
|
for label in labels:
|
|
label_id = label['id']
|
|
del rm.metering_labels[label_id]
|
|
|
|
@log_helpers.log_method_call
|
|
def add_metering_label(self, context, routers):
|
|
for router in routers:
|
|
self._process_associate_metering_label(router)
|
|
|
|
@log_helpers.log_method_call
|
|
def add_metering_label_rule(self, context, routers):
|
|
for router in routers:
|
|
self._add_metering_label_rule(router)
|
|
|
|
@log_helpers.log_method_call
|
|
def remove_metering_label_rule(self, context, routers):
|
|
for router in routers:
|
|
self._remove_metering_label_rule(router)
|
|
|
|
@log_helpers.log_method_call
|
|
def update_metering_label_rules(self, context, routers):
|
|
for router in routers:
|
|
self._update_metering_label_rules(router)
|
|
|
|
def _add_metering_label_rule(self, router):
|
|
self._process_metering_rule_action(router, 'create')
|
|
|
|
def _remove_metering_label_rule(self, router):
|
|
self._process_metering_rule_action(router, 'delete')
|
|
|
|
def _create_metering_label_chain(self, rm, label_chain, rules_chain):
|
|
rm.iptables_manager.ipv4['filter'].add_chain(label_chain, wrap=False)
|
|
rm.iptables_manager.ipv4['filter'].add_chain(rules_chain, wrap=False)
|
|
rm.iptables_manager.ipv4['filter'].add_rule(
|
|
TOP_CHAIN, '-j ' + rules_chain, wrap=False)
|
|
rm.iptables_manager.ipv4['filter'].add_rule(
|
|
label_chain, '', wrap=False)
|
|
|
|
def _process_metering_rule_action_based_on_ns(
|
|
self, router, action, ext_dev, im):
|
|
'''Process metering rule actions based specific namespaces.'''
|
|
rm = self.routers.get(router['id'])
|
|
with IptablesManagerTransaction(im):
|
|
labels = router.get(constants.METERING_LABEL_KEY, [])
|
|
for label in labels:
|
|
label_id = label['id']
|
|
label_chain = iptables_manager.get_chain_name(
|
|
WRAP_NAME + LABEL + label_id, wrap=False)
|
|
|
|
rules_chain = iptables_manager.get_chain_name(
|
|
WRAP_NAME + RULE + label_id, wrap=False)
|
|
|
|
exists = rm.metering_labels.get(label_id)
|
|
if action == 'create' and not exists:
|
|
self._create_metering_label_chain(rm,
|
|
label_chain,
|
|
rules_chain)
|
|
rm.metering_labels[label_id] = label
|
|
|
|
rule = label.get('rule')
|
|
if rule:
|
|
if action == 'create':
|
|
self._process_metering_label_rule_add(
|
|
rule, ext_dev, label_chain, rules_chain, im)
|
|
elif action == 'delete':
|
|
self._process_metering_label_rule_delete(
|
|
rule, ext_dev, label_chain, rules_chain, im)
|
|
|
|
def _process_metering_rule_action(self, router, action):
|
|
rm = self.routers.get(router['id'])
|
|
if not rm:
|
|
return
|
|
|
|
ext_dev, ext_snat_dev = self.get_external_device_names(rm)
|
|
for (im, dev) in [(rm.iptables_manager, ext_dev),
|
|
(rm.snat_iptables_manager, ext_snat_dev)]:
|
|
if im and dev:
|
|
self._process_metering_rule_action_based_on_ns(
|
|
router, action, dev, im)
|
|
|
|
def _update_metering_label_rules_based_on_ns(self, router, ext_dev, im):
|
|
'''Update metering lable rules based on namespace.'''
|
|
with IptablesManagerTransaction(im):
|
|
labels = router.get(constants.METERING_LABEL_KEY, [])
|
|
for label in labels:
|
|
label_id = label['id']
|
|
|
|
label_chain = iptables_manager.get_chain_name(
|
|
WRAP_NAME + LABEL + label_id, wrap=False)
|
|
rules_chain = iptables_manager.get_chain_name(
|
|
WRAP_NAME + RULE + label_id, wrap=False)
|
|
im.ipv4['filter'].empty_chain(rules_chain, wrap=False)
|
|
|
|
rules = label.get('rules')
|
|
if rules:
|
|
self._process_metering_label_rules(
|
|
rules, label_chain, rules_chain, ext_dev, im)
|
|
|
|
def _update_metering_label_rules(self, router):
|
|
rm = self.routers.get(router['id'])
|
|
if not rm:
|
|
return
|
|
|
|
ext_dev, ext_snat_dev = self.get_external_device_names(rm)
|
|
for (im, dev) in [(rm.iptables_manager, ext_dev),
|
|
(rm.snat_iptables_manager, ext_snat_dev)]:
|
|
if im and dev:
|
|
self._update_metering_label_rules_based_on_ns(router, dev, im)
|
|
|
|
@log_helpers.log_method_call
|
|
def remove_metering_label(self, context, routers):
|
|
for router in routers:
|
|
self._process_disassociate_metering_label(router)
|
|
|
|
@log_helpers.log_method_call
|
|
def get_traffic_counters(self, context, routers):
|
|
accs = {}
|
|
routers_to_reconfigure = set()
|
|
for router in routers:
|
|
rm = self.routers.get(router['id'])
|
|
if not rm:
|
|
continue
|
|
|
|
for label_id in rm.metering_labels:
|
|
try:
|
|
chain = iptables_manager.get_chain_name(WRAP_NAME +
|
|
LABEL +
|
|
label_id,
|
|
wrap=False)
|
|
|
|
chain_acc = rm.iptables_manager.get_traffic_counters(
|
|
chain, wrap=False, zero=True)
|
|
except RuntimeError:
|
|
LOG.exception('Failed to get traffic counters, '
|
|
'router: %s', router)
|
|
routers_to_reconfigure.add(router['id'])
|
|
continue
|
|
|
|
if not chain_acc:
|
|
continue
|
|
|
|
acc = accs.get(label_id, {'pkts': 0, 'bytes': 0})
|
|
|
|
acc['pkts'] += chain_acc['pkts']
|
|
acc['bytes'] += chain_acc['bytes']
|
|
|
|
accs[label_id] = acc
|
|
|
|
for router_id in routers_to_reconfigure:
|
|
del self.routers[router_id]
|
|
|
|
return accs
|