1 # Copyright (c) 2015 Hewlett-Packard Enterprise Development Company, L.P.
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may
4 # not use this file except in compliance with the License. You may obtain
5 # a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations
19 from oslo_log import log as logging
21 from neutron.agent.common import utils as common_utils
22 from neutron.agent.linux import ip_lib
23 from neutron.common import constants
24 from neutron.common import exceptions
25 from neutron.common import utils
27 LOG = logging.getLogger(__name__)
30 class NamespaceEtcDir(object):
31 """Creates a directory where namespace local /etc/iproute2 files can live
33 Directories are created under /etc/netns/<namespace_name>/iproute2 so that
34 when you exec a command inside a namespace, the directory is available as
35 /etc/iproute2 locally to the namespace.
37 The directory ownership is changed to the owner of the L3 agent process
38 so that root is no longer required to manage the file. This limits the
39 scope of where root is needed. Changing ownership is justified because
40 the directory lives under a namespace specific sub-directory of /etc, it
41 should be considered owned by the L3 agent process, which also manages the
44 The directory and its contents should not be considered config. Nothing
45 needs to be done for upgrade. The only reason for it to live under /etc
46 within the namespace is that is the only place from where the ip command
50 BASE_DIR = "/etc/netns"
52 def __init__(self, namespace):
53 self._directory = os.path.join(self.BASE_DIR, namespace)
56 common_utils.execute(['mkdir', '-p', self._directory],
59 user_id = os.geteuid()
60 common_utils.execute(['chown', user_id, self._directory],
64 common_utils.execute(['rm', '-r', '-f', self._directory],
67 def get_full_path(self):
68 return self._directory
71 class RoutingTable(object):
72 def __init__(self, namespace, table_id, name):
74 self.table_id = table_id
75 self.ip_route = ip_lib.IPRoute(namespace=namespace, table=name)
78 def __eq__(self, other):
79 return self.table_id == other.table_id
84 def add(self, device, cidr):
85 table = device.route.table(self.name)
86 cidr = netaddr.IPNetwork(cidr)
87 # Get the network cidr (e.g. 192.168.5.135/23 -> 192.168.4.0/23)
88 net = utils.ip_to_cidr(cidr.network, cidr.prefixlen)
89 self._keep.add((net, device.name))
90 table.add_onlink_route(net)
92 def add_gateway(self, device, gateway_ip):
93 table = device.route.table(self.name)
94 ip_version = ip_lib.get_ip_version(gateway_ip)
95 self._keep.add((constants.IP_ANY[ip_version], device.name))
96 table.add_gateway(gateway_ip)
102 def __exit__(self, exc_type, value, traceback):
109 ipv4_routes = self.ip_route.route.list_routes(constants.IP_VERSION_4)
110 ipv6_routes = self.ip_route.route.list_routes(constants.IP_VERSION_6)
111 all_routes = {(r['cidr'], r['dev'])
112 for r in ipv4_routes + ipv6_routes}
114 for cidr, dev in all_routes - keep:
116 self.ip_route.route.delete_route(cidr, dev=dev)
117 except exceptions.DeviceNotFoundError:
123 class RoutingTablesManager(object):
124 """Manages mapping from routing table name to routing tables
126 The iproute2 package can read a mapping from /etc/iproute2/rt_tables. When
127 namespaces are used, it is possible to maintain an rt_tables file that is
128 unique to the namespace.
130 It is necessary to maintain this mapping on disk somewhere because it must
131 survive agent restarts. Otherwise, we'd be remapping each time. It is not
132 necessary to maintain it in the Neutron database because it is an
133 agent-local implementation detail.
135 While it could be kept in any local file, it is convenient to keep it in
136 the rt_tables file so that we can simply pass the table name to the
137 ip route commands. It will also be helpful for debugging to be able to use
138 the table name on the command line manually.
141 FILENAME = 'iproute2/rt_tables'
142 ALL_IDS = set(range(1024, 2048))
143 DEFAULT_TABLES = {"local": 255,
148 def __init__(self, namespace):
149 self._namespace = namespace
150 self.etc = NamespaceEtcDir(namespace)
151 self._rt_tables_filename = os.path.join(
152 self.etc.get_full_path(), self.FILENAME)
154 self.initialize_map()
156 def initialize_map(self):
157 # Create a default table if one is not already found
159 utils.ensure_dir(os.path.dirname(self._rt_tables_filename))
160 if not os.path.exists(self._rt_tables_filename):
161 self._write_map(self.DEFAULT_TABLES)
164 def _get_or_create(self, table_id, table_name):
165 table = self._tables.get(table_id)
167 self._tables[table_id] = table = RoutingTable(
168 self._namespace, table_id, table_name)
171 def get(self, table_name):
172 """Returns the table ID for the given table name"""
173 table_id = self._read_map().get(table_name)
174 if table_id is not None:
175 return self._get_or_create(table_id, table_name)
178 return set(self._get_or_create(t_id, name)
179 for name, t_id in self._read_map().items())
181 def add(self, table_name):
182 """Ensures there is a single table id available for the table name"""
183 name_to_id = self._read_map()
185 def get_and_keep(table_id, table_name):
186 table = self._get_or_create(table_id, table_name)
187 self._keep.add(table)
190 # If it is already there, just return it.
191 if table_name in name_to_id:
192 return get_and_keep(name_to_id[table_name], table_name)
194 # Otherwise, find an available id and write the new file
195 table_ids = set(name_to_id.values())
196 available_ids = self.ALL_IDS - table_ids
197 name_to_id[table_name] = table_id = available_ids.pop()
198 self._write_map(name_to_id)
199 return get_and_keep(table_id, table_name)
201 def delete(self, table_name):
202 """Removes the table from the file"""
203 name_to_id = self._read_map()
205 # If it is already there, remove it
206 table_id = name_to_id.pop(table_name, None)
207 self._tables.pop(table_id, None)
210 self._write_map(name_to_id)
212 def _write_map(self, name_to_id):
214 for name, table_id in name_to_id.items():
215 buf.write("%s\t%s\n" % (table_id, name))
216 utils.replace_file(self._rt_tables_filename, buf.getvalue())
220 with open(self._rt_tables_filename, "r") as rt_file:
222 fields = line.split()
225 table_id_str, name = fields
227 table_id = int(table_id_str)
230 result[name] = table_id
237 for rt in self.get_all():
238 if rt.table_id not in self.DEFAULT_TABLES.values():
243 def __exit__(self, exc_type, value, traceback):
247 all_tables = set(rt for rt in self.get_all()
248 if rt.table_id not in self.DEFAULT_TABLES.values())
249 for rt in all_tables:
250 rt.__exit__(None, None, None)
252 for rt in all_tables - self._keep: