neutron/neutron/services/metering/drivers/iptables/iptables_driver.py

376 lines
15 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 oslo_utils import importutils
import six
from neutron._i18n import _, _LE, _LI
from neutron.agent.common import config
from neutron.agent.linux import interface
from neutron.agent.linux import iptables_manager
from neutron.common import constants
from neutron.common import ipv6_utils
from neutron.services.metering.drivers import abstract_driver
LOG = logging.getLogger(__name__)
NS_PREFIX = 'qrouter-'
WRAP_NAME = 'neutron-meter'
EXTERNAL_DEV_PREFIX = 'qg-'
TOP_CHAIN = WRAP_NAME + "-FORWARD"
RULE = '-r-'
LABEL = '-l-'
config.register_interface_driver_opts_helper(cfg.CONF)
cfg.CONF.register_opts(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 = 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 = {}
if not self.conf.interface_driver:
raise SystemExit(_('An interface driver must be specified'))
LOG.info(_LI("Loading interface driver %s"),
self.conf.interface_driver)
self.driver = importutils.import_object(self.conf.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 six.iteritems(self.routers):
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:
with IptablesManagerTransaction(old_rm.iptables_manager):
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_name(self, port_id):
return (EXTERNAL_DEV_PREFIX + port_id)[:self.driver.DEV_NAME_LEN]
def _process_metering_label_rules(self, rm, rules, label_chain,
rules_chain):
im = rm.iptables_manager
ex_gw_port = rm.router.get('gw_port_id')
if not ex_gw_port:
return
ext_dev = self.get_external_device_name(ex_gw_port)
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, rm, rule, ext_dev,
label_chain, rules_chain):
im = rm.iptables_manager
self._add_rule_to_chain(ext_dev, rule, im, label_chain, rules_chain)
def _process_metering_label_rule_delete(self, rm, rule, ext_dev,
label_chain, rules_chain):
im = rm.iptables_manager
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 = '-d %s -o %s' % (remote_ip, ext_dev)
else:
dir_opt = '-s %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_associate_metering_label(self, router):
self._update_router(router)
rm = self.routers.get(router['id'])
with IptablesManagerTransaction(rm.iptables_manager):
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)
rm.iptables_manager.ipv4['filter'].add_chain(label_chain,
wrap=False)
rules_chain = iptables_manager.get_chain_name(WRAP_NAME +
RULE + label_id,
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)
rules = label.get('rules')
if rules:
self._process_metering_label_rules(rm, rules,
label_chain,
rules_chain)
rm.metering_labels[label_id] = label
def _process_disassociate_metering_label(self, router):
rm = self.routers.get(router['id'])
if not rm:
return
with IptablesManagerTransaction(rm.iptables_manager):
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)
rm.iptables_manager.ipv4['filter'].remove_chain(label_chain,
wrap=False)
rm.iptables_manager.ipv4['filter'].remove_chain(rules_chain,
wrap=False)
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 _process_metering_rule_action(self, router, action):
rm = self.routers.get(router['id'])
if not rm:
return
ext_dev = self.get_external_device_name(rm.router['gw_port_id'])
if not ext_dev:
return
with IptablesManagerTransaction(rm.iptables_manager):
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)
rule = label.get('rule')
if rule:
if action == 'create':
self._process_metering_label_rule_add(rm, rule,
ext_dev,
label_chain,
rules_chain)
elif action == 'delete':
self._process_metering_label_rule_delete(rm, rule,
ext_dev,
label_chain,
rules_chain)
def _update_metering_label_rules(self, router):
rm = self.routers.get(router['id'])
if not rm:
return
with IptablesManagerTransaction(rm.iptables_manager):
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)
rm.iptables_manager.ipv4['filter'].empty_chain(rules_chain,
wrap=False)
rules = label.get('rules')
if rules:
self._process_metering_label_rules(rm, rules,
label_chain,
rules_chain)
@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 = {}
for router in routers:
rm = self.routers.get(router['id'])
if not rm:
continue
for label_id, label in rm.metering_labels.items():
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(_LE('Failed to get traffic counters, '
'router: %s'), router)
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
return accs