--- /dev/null
+# Copyright (c) 2015 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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 netaddr
+import os
+import six
+
+from oslo_log import log as logging
+
+from neutron.agent.common import utils as common_utils
+from neutron.agent.linux import ip_lib
+from neutron.common import constants
+from neutron.common import exceptions
+from neutron.common import utils
+
+LOG = logging.getLogger(__name__)
+
+
+class NamespaceEtcDir(object):
+ """Creates a directory where namespace local /etc/iproute2 files can live
+
+ Directories are created under /etc/netns/<namespace_name>/iproute2 so that
+ when you exec a command inside a namespace, the directory is available as
+ /etc/iproute2 locally to the namespace.
+
+ The directory ownership is changed to the owner of the L3 agent process
+ so that root is no longer required to manage the file. This limits the
+ scope of where root is needed. Changing ownership is justified because
+ the directory lives under a namespace specific sub-directory of /etc, it
+ should be considered owned by the L3 agent process, which also manages the
+ namespace itself.
+
+ The directory and its contents should not be considered config. Nothing
+ needs to be done for upgrade. The only reason for it to live under /etc
+ within the namespace is that is the only place from where the ip command
+ will read it.
+ """
+
+ BASE_DIR = "/etc/netns"
+
+ def __init__(self, namespace):
+ self._directory = os.path.join(self.BASE_DIR, namespace)
+
+ def create(self):
+ common_utils.execute(['mkdir', '-p', self._directory],
+ run_as_root=True)
+
+ user_id = os.geteuid()
+ common_utils.execute(['chown', user_id, self._directory],
+ run_as_root=True)
+
+ def destroy(self):
+ common_utils.execute(['rm', '-r', '-f', self._directory],
+ run_as_root=True)
+
+ def get_full_path(self):
+ return self._directory
+
+
+class RoutingTable(object):
+ def __init__(self, namespace, table_id, name):
+ self.name = name
+ self.table_id = table_id
+ self.ip_route = ip_lib.IPRoute(namespace=namespace, table=name)
+ self._keep = set()
+
+ def __eq__(self, other):
+ return self.table_id == other.table_id
+
+ def __hash__(self):
+ return self.table_id
+
+ def add(self, device, cidr):
+ table = device.route.table(self.name)
+ cidr = netaddr.IPNetwork(cidr)
+ # Get the network cidr (e.g. 192.168.5.135/23 -> 192.168.4.0/23)
+ net = utils.ip_to_cidr(cidr.network, cidr.prefixlen)
+ self._keep.add((net, device.name))
+ table.add_onlink_route(net)
+
+ def add_gateway(self, device, gateway_ip):
+ table = device.route.table(self.name)
+ ip_version = ip_lib.get_ip_version(gateway_ip)
+ self._keep.add((constants.IP_ANY[ip_version], device.name))
+ table.add_gateway(gateway_ip)
+
+ def __enter__(self):
+ self._keep = set()
+ return self
+
+ def __exit__(self, exc_type, value, traceback):
+ if exc_type:
+ return False
+
+ keep = self._keep
+ self._keep = None
+
+ ipv4_routes = self.ip_route.route.list_routes(constants.IP_VERSION_4)
+ ipv6_routes = self.ip_route.route.list_routes(constants.IP_VERSION_6)
+ all_routes = {(r['cidr'], r['dev'])
+ for r in ipv4_routes + ipv6_routes}
+
+ for cidr, dev in all_routes - keep:
+ try:
+ self.ip_route.route.delete_route(cidr, dev=dev)
+ except exceptions.DeviceNotFoundError:
+ pass
+
+ return True
+
+
+class RoutingTablesManager(object):
+ """Manages mapping from routing table name to routing tables
+
+ The iproute2 package can read a mapping from /etc/iproute2/rt_tables. When
+ namespaces are used, it is possible to maintain an rt_tables file that is
+ unique to the namespace.
+
+ It is necessary to maintain this mapping on disk somewhere because it must
+ survive agent restarts. Otherwise, we'd be remapping each time. It is not
+ necessary to maintain it in the Neutron database because it is an
+ agent-local implementation detail.
+
+ While it could be kept in any local file, it is convenient to keep it in
+ the rt_tables file so that we can simply pass the table name to the
+ ip route commands. It will also be helpful for debugging to be able to use
+ the table name on the command line manually.
+ """
+
+ FILENAME = 'iproute2/rt_tables'
+ ALL_IDS = set(range(1024, 2048))
+ DEFAULT_TABLES = {"local": 255,
+ "main": 254,
+ "default": 253,
+ "unspec": 0}
+
+ def __init__(self, namespace):
+ self._namespace = namespace
+ self.etc = NamespaceEtcDir(namespace)
+ self._rt_tables_filename = os.path.join(
+ self.etc.get_full_path(), self.FILENAME)
+ self._tables = {}
+ self.initialize_map()
+
+ def initialize_map(self):
+ # Create a default table if one is not already found
+ self.etc.create()
+ utils.ensure_dir(os.path.dirname(self._rt_tables_filename))
+ if not os.path.exists(self._rt_tables_filename):
+ self._write_map(self.DEFAULT_TABLES)
+ self._keep = set()
+
+ def _get_or_create(self, table_id, table_name):
+ table = self._tables.get(table_id)
+ if not table:
+ self._tables[table_id] = table = RoutingTable(
+ self._namespace, table_id, table_name)
+ return table
+
+ def get(self, table_name):
+ """Returns the table ID for the given table name"""
+ table_id = self._read_map().get(table_name)
+ if table_id is not None:
+ return self._get_or_create(table_id, table_name)
+
+ def get_all(self):
+ return set(self._get_or_create(t_id, name)
+ for name, t_id in self._read_map().items())
+
+ def add(self, table_name):
+ """Ensures there is a single table id available for the table name"""
+ name_to_id = self._read_map()
+
+ def get_and_keep(table_id, table_name):
+ table = self._get_or_create(table_id, table_name)
+ self._keep.add(table)
+ return table
+
+ # If it is already there, just return it.
+ if table_name in name_to_id:
+ return get_and_keep(name_to_id[table_name], table_name)
+
+ # Otherwise, find an available id and write the new file
+ table_ids = set(name_to_id.values())
+ available_ids = self.ALL_IDS - table_ids
+ name_to_id[table_name] = table_id = available_ids.pop()
+ self._write_map(name_to_id)
+ return get_and_keep(table_id, table_name)
+
+ def delete(self, table_name):
+ """Removes the table from the file"""
+ name_to_id = self._read_map()
+
+ # If it is already there, remove it
+ table_id = name_to_id.pop(table_name, None)
+ self._tables.pop(table_id, None)
+
+ # Write the new file
+ self._write_map(name_to_id)
+
+ def _write_map(self, name_to_id):
+ buf = six.StringIO()
+ for name, table_id in name_to_id.items():
+ buf.write("%s\t%s\n" % (table_id, name))
+ utils.replace_file(self._rt_tables_filename, buf.getvalue())
+
+ def _read_map(self):
+ result = {}
+ with open(self._rt_tables_filename, "r") as rt_file:
+ for line in rt_file:
+ fields = line.split()
+ if len(fields) != 2:
+ continue
+ table_id_str, name = fields
+ try:
+ table_id = int(table_id_str)
+ except ValueError:
+ continue
+ result[name] = table_id
+ return result
+
+ def destroy(self):
+ self.etc.destroy()
+
+ def __enter__(self):
+ for rt in self.get_all():
+ if rt.table_id not in self.DEFAULT_TABLES.values():
+ rt.__enter__()
+ self._keep = set()
+ return self
+
+ def __exit__(self, exc_type, value, traceback):
+ if exc_type:
+ return False
+
+ all_tables = set(rt for rt in self.get_all()
+ if rt.table_id not in self.DEFAULT_TABLES.values())
+ for rt in all_tables:
+ rt.__exit__(None, None, None)
+
+ for rt in all_tables - self._keep:
+ self.delete(rt.name)
+
+ return True
--- /dev/null
+# Copyright (c) 2015 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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 fixtures
+import mock
+
+from neutron.agent.common import utils as common_utils
+from neutron.agent.l3 import rt_tables
+from neutron.tests import base
+
+
+def mock_netnamespace_directory(function):
+ """Decorator to test RoutingTablesManager with temp dir
+
+ Allows direct testing of RoutingTablesManager by changing the directory
+ where it finds the rt_tables to one in /tmp where root privileges are not
+ required and it won't mess with any real routing tables.
+ """
+ orig_execute = common_utils.execute
+
+ def execute_no_root(*args, **kwargs):
+ kwargs['run_as_root'] = False
+ orig_execute(*args, **kwargs)
+
+ def inner(*args, **kwargs):
+ with fixtures.TempDir() as tmpdir:
+ cls = rt_tables.NamespaceEtcDir
+ with mock.patch.object(common_utils, 'execute') as execute,\
+ mock.patch.object(cls, 'BASE_DIR', tmpdir.path):
+ execute.side_effect = execute_no_root
+ function(*args, **kwargs)
+ return inner
+
+
+class TestRoutingTablesManager(base.BaseTestCase):
+ def setUp(self):
+ super(TestRoutingTablesManager, self).setUp()
+ self.ns_name = "fakens"
+
+ @mock_netnamespace_directory
+ def test_default_tables(self):
+ rtm = rt_tables.RoutingTablesManager(self.ns_name)
+ self.assertEqual(253, rtm.get("default").table_id)
+ self.assertEqual(254, rtm.get("main").table_id)
+ self.assertEqual(255, rtm.get("local").table_id)
+ self.assertEqual(0, rtm.get("unspec").table_id)
+
+ @mock_netnamespace_directory
+ def test_get_all(self):
+ rtm = rt_tables.RoutingTablesManager(self.ns_name)
+ table_names = set(rt.name for rt in rtm.get_all())
+ self.assertEqual({"main", "default", "local", "unspec"}, table_names)
+
+ new_table = rtm.add("faketable")
+ self.assertIn(new_table, rtm.get_all())
+
+ @mock_netnamespace_directory
+ def test_add(self):
+ rtm = rt_tables.RoutingTablesManager(self.ns_name)
+ added_table = rtm.add("faketable")
+ self.assertGreaterEqual(added_table.table_id, 1024)
+
+ table = rtm.get("faketable")
+ self.assertEqual(added_table, table)
+
+ # Be sure that adding it twice gets the same result
+ added_again = rtm.add("faketable")
+ self.assertEqual(added_table, added_again)
+
+ @mock_netnamespace_directory
+ def test_delete(self):
+ rtm = rt_tables.RoutingTablesManager(self.ns_name)
+ rtm.add("faketable")
+ rtm.delete("faketable")
+
+ table = rtm.get("faketable")
+ self.assertIsNone(table)