Set lock_path correctly.
[openstack-build/neutron-build.git] / neutron / agent / l3 / rt_tables.py
1 # Copyright (c) 2015 Hewlett-Packard Enterprise Development Company, L.P.
2 #
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
6 #
7 #         http://www.apache.org/licenses/LICENSE-2.0
8 #
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
13 #    under the License.
14
15 import netaddr
16 import os
17 import six
18
19 from oslo_log import log as logging
20
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
26
27 LOG = logging.getLogger(__name__)
28
29
30 class NamespaceEtcDir(object):
31     """Creates a directory where namespace local /etc/iproute2 files can live
32
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.
36
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
42     namespace itself.
43
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
47     will read it.
48     """
49
50     BASE_DIR = "/etc/netns"
51
52     def __init__(self, namespace):
53         self._directory = os.path.join(self.BASE_DIR, namespace)
54
55     def create(self):
56         common_utils.execute(['mkdir', '-p', self._directory],
57                              run_as_root=True)
58
59         user_id = os.geteuid()
60         common_utils.execute(['chown', user_id, self._directory],
61                              run_as_root=True)
62
63     def destroy(self):
64         common_utils.execute(['rm', '-r', '-f', self._directory],
65                              run_as_root=True)
66
67     def get_full_path(self):
68         return self._directory
69
70
71 class RoutingTable(object):
72     def __init__(self, namespace, table_id, name):
73         self.name = name
74         self.table_id = table_id
75         self.ip_route = ip_lib.IPRoute(namespace=namespace, table=name)
76         self._keep = set()
77
78     def __eq__(self, other):
79         return self.table_id == other.table_id
80
81     def __hash__(self):
82         return self.table_id
83
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)
91
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)
97
98     def __enter__(self):
99         self._keep = set()
100         return self
101
102     def __exit__(self, exc_type, value, traceback):
103         if exc_type:
104             return False
105
106         keep = self._keep
107         self._keep = None
108
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}
113
114         for cidr, dev in all_routes - keep:
115             try:
116                 self.ip_route.route.delete_route(cidr, dev=dev)
117             except exceptions.DeviceNotFoundError:
118                 pass
119
120         return True
121
122
123 class RoutingTablesManager(object):
124     """Manages mapping from routing table name to routing tables
125
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.
129
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.
134
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.
139     """
140
141     FILENAME = 'iproute2/rt_tables'
142     ALL_IDS = set(range(1024, 2048))
143     DEFAULT_TABLES = {"local": 255,
144                       "main": 254,
145                       "default": 253,
146                       "unspec": 0}
147
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)
153         self._tables = {}
154         self.initialize_map()
155
156     def initialize_map(self):
157         # Create a default table if one is not already found
158         self.etc.create()
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)
162         self._keep = set()
163
164     def _get_or_create(self, table_id, table_name):
165         table = self._tables.get(table_id)
166         if not table:
167             self._tables[table_id] = table = RoutingTable(
168                  self._namespace, table_id, table_name)
169         return table
170
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)
176
177     def get_all(self):
178         return set(self._get_or_create(t_id, name)
179                    for name, t_id in self._read_map().items())
180
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()
184
185         def get_and_keep(table_id, table_name):
186             table = self._get_or_create(table_id, table_name)
187             self._keep.add(table)
188             return table
189
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)
193
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)
200
201     def delete(self, table_name):
202         """Removes the table from the file"""
203         name_to_id = self._read_map()
204
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)
208
209         # Write the new file
210         self._write_map(name_to_id)
211
212     def _write_map(self, name_to_id):
213         buf = six.StringIO()
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())
217
218     def _read_map(self):
219         result = {}
220         with open(self._rt_tables_filename, "r") as rt_file:
221             for line in rt_file:
222                 fields = line.split()
223                 if len(fields) != 2:
224                     continue
225                 table_id_str, name = fields
226                 try:
227                     table_id = int(table_id_str)
228                 except ValueError:
229                     continue
230                 result[name] = table_id
231         return result
232
233     def destroy(self):
234         self.etc.destroy()
235
236     def __enter__(self):
237         for rt in self.get_all():
238             if rt.table_id not in self.DEFAULT_TABLES.values():
239                 rt.__enter__()
240         self._keep = set()
241         return self
242
243     def __exit__(self, exc_type, value, traceback):
244         if exc_type:
245             return False
246
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)
251
252         for rt in all_tables - self._keep:
253             self.delete(rt.name)
254
255         return True