Merge ~chris.macnaughton/ubuntu/+source/neutron:bug/1881157 into ~ubuntu-server-dev/ubuntu/+source/neutron:stable/queens

Proposed by Chris MacNaughton
Status: Needs review
Proposed branch: ~chris.macnaughton/ubuntu/+source/neutron:bug/1881157
Merge into: ~ubuntu-server-dev/ubuntu/+source/neutron:stable/queens
Diff against target: 425 lines (+403/-0)
3 files modified
debian/changelog (+7/-0)
debian/patches/lp1881157-remote-sg-is-left-behind.patch (+395/-0)
debian/patches/series (+1/-0)
Reviewer Review Type Date Requested Status
Corey Bryant Approve
Review via email: mp+391122@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Corey Bryant (corey.bryant) wrote :

Seems fine on a quick glance. We'll need to get point releases uploaded for > queens before we can upload this one. Let's hold off on merging until then in case something else comes up ahead of this.

Revision history for this message
Corey Bryant (corey.bryant) wrote :

Diff with stable/queens patch looks good. Looks like there was a quilt refresh to adapt to the current state of the package branch. Approving for now but not merging yet.

Revision history for this message
Corey Bryant (corey.bryant) :
review: Approve
Revision history for this message
Chris MacNaughton (chris.macnaughton) wrote :

Agreed - ideally we'll get that neutron point release out soon which will give us S/T/U more easily!

Unmerged commits

0489eea... by Chris MacNaughton

d/p/lp1881157-remote-sg-is-left-behind.patch: Backport fix for security group IDs being left behind (LP: #1881157).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/debian/changelog b/debian/changelog
index 75f60a5..70e65db 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
1neutron (2:12.1.1-0ubuntu3) UNRELEASED; urgency=medium
2
3 * d/p/lp1881157-remote-sg-is-left-behind.patch: Backport fix for security
4 group IDs being left behind (LP: #1881157).
5
6 -- Chris MacNaughton <chris.macnaughton@canonical.com> Tue, 22 Sep 2020 11:16:27 +0000
7
1neutron (2:12.1.1-0ubuntu2) bionic; urgency=medium8neutron (2:12.1.1-0ubuntu2) bionic; urgency=medium
29
3 * d/p/Ensure-fip-ip-rules-deleted-when-fip-removed.patch10 * d/p/Ensure-fip-ip-rules-deleted-when-fip-removed.patch
diff --git a/debian/patches/lp1881157-remote-sg-is-left-behind.patch b/debian/patches/lp1881157-remote-sg-is-left-behind.patch
4new file mode 10064411new file mode 100644
index 0000000..fda14bc
--- /dev/null
+++ b/debian/patches/lp1881157-remote-sg-is-left-behind.patch
@@ -0,0 +1,395 @@
1From d0b696f076a94830b040ffcb2dec28e2eabe2e04 Mon Sep 17 00:00:00 2001
2From: Rodolfo Alonso Hernandez <ralonsoh@redhat.com>
3Date: Tue, 02 Jun 2020 17:09:07 +0000
4Subject: [PATCH] [OVS][FW] Remote SG IDs left behind when a SG is removed
5
6When any port in the OVS agent is using a security groups (SG) and
7this SG is removed, is marked to be deleted. This deletion process
8is done in [1].
9
10The SG deletion process consists on removing any reference of this SG
11from the firewall and the SG port map. The firewall removes this SG in
12[2].
13
14The information of a SG is stored in:
15* ConjIPFlowManager.conj_id_map = ConjIdMap(). This class stores the
16 conjunction IDS (conj_ids) in a dictionary using the following keys:
17
18 ConjIdMap.id_map[(sg_id, remote_sg_id, direction, ethertype,
19 conj_ids)] = conj_id_XXX
20
21* ConjIPFlowManager.conj_ids is a nested dictionary, built in the
22 following way:
23
24 self.conj_ids[vlan_tag][(direction, ethertype)][remote_sg_id] = \
25 set([conj_id_1, conj_id_2, ...])
26
27This patch stores all conjuntion IDs generated and assigned to the
28tuple (sg_id, remote_sg_id, direction, ethertype). When a SG is
29removed, the deletion method will look for this SG in the new storage
30variable created, ConjIdMap.id_map_group, and will mark all the
31conjuntion IDs related to be removed. That will cleanup those rules
32left in the OVS matching:
33 action=conjunction(conj_id, 1/2)
34
35[1]https://github.com/openstack/neutron/blob/118930f03d31f157f8c7a9e6c57122ecea8982b9/neutron/agent/linux/openvswitch_firewall/firewall.py#L731
36[2]https://github.com/openstack/neutron/blob/118930f03d31f157f8c7a9e6c57122ecea8982b9/neutron/agent/linux/openvswitch_firewall/firewall.py#L399
37
38Conflicts:
39 neutron/agent/linux/openvswitch_firewall/firewall.py
40 neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py
41
42Change-Id: I63e446a30cf10e7bcd34a6f0d6ba1711301efcbe
43Related-Bug: #1881157
44(cherry picked from commit 0eebd002ccda66dc6d9f9e5a254815109225e299)
45(cherry picked from commit ed22f7a2ff19a874bc8521f84cb4fd1c7483a23f)
46(cherry picked from commit 6615f248e25a361d988db1795a3486a58f4768cb)
47---
48
49diff --git a/neutron/agent/linux/openvswitch_firewall/firewall.py b/neutron/agent/linux/openvswitch_firewall/firewall.py
50index 3570032..b8522b2 100644
51--- a/neutron/agent/linux/openvswitch_firewall/firewall.py
52+++ b/neutron/agent/linux/openvswitch_firewall/firewall.py
53@@ -268,6 +268,10 @@
54
55 def __init__(self):
56 self.id_map = collections.defaultdict(self._conj_id_factory)
57+ # Stores the set of conjuntion IDs used for each unique tuple
58+ # (sg_id, remote_sg_id, direction, ethertype). Each tuple can have up
59+ # to 8 conjuntion IDs (see ConjIPFlowManager.add()).
60+ self.id_map_group = collections.defaultdict(set)
61 self.id_free = collections.deque()
62 self.max_id = 0
63
64@@ -298,12 +302,21 @@
65 return a list of (remote_sg_id, conj_id), which are no longer
66 in use.
67 """
68- result = []
69+ result = set([])
70 for k in list(self.id_map.keys()):
71 if sg_id in k[0:2]:
72 conj_id = self.id_map.pop(k)
73- result.append((k[1], conj_id))
74+ result.add((k[1], conj_id))
75 self.id_free.append(conj_id)
76+
77+ # If the remote_sg_id is removed, the tuple (sg_id, remote_sg_id,
78+ # direction, ethertype) no longer exists; the conjunction IDs assigned
79+ # to this tuple should be removed too.
80+ for k in list(self.id_map_group.keys()):
81+ if sg_id in k[0:2]:
82+ conj_id_groups = self.id_map_group.pop(k, [])
83+ for conj_id in conj_id_groups:
84+ result.add((k[1], conj_id))
85
86 return result
87
88@@ -346,12 +359,22 @@
89 return addr_to_conj
90
91 def _update_flows_for_vlan_subr(self, direction, ethertype, vlan_tag,
92- flow_state, addr_to_conj):
93+ flow_state, addr_to_conj,
94+ conj_id_to_remove):
95 """Do the actual flow updates for given direction and ethertype."""
96- current_ips = set(flow_state.keys())
97- self.driver.delete_flows_for_ip_addresses(
98- current_ips - set(addr_to_conj.keys()),
99- direction, ethertype, vlan_tag)
100+ conj_id_to_remove = conj_id_to_remove or []
101+ # Delete any current flow related to any deleted IP address, before
102+ # creating the flows for the current IPs.
103+ self.driver.delete_flows_for_flow_state(
104+ flow_state, addr_to_conj, direction, ethertype, vlan_tag)
105+ for conj_id_set in conj_id_to_remove:
106+ # Remove any remaining flow with remote SG ID conj_id_to_remove
107+ for current_ip, conj_ids in flow_state.items():
108+ conj_ids_to_remove = conj_id_set & set(conj_ids)
109+ self.driver.delete_flow_for_ip(
110+ current_ip, direction, ethertype, vlan_tag,
111+ conj_ids_to_remove)
112+
113 for addr, conj_ids in addr_to_conj.items():
114 conj_ids.sort()
115 if flow_state.get(addr) == conj_ids:
116@@ -360,7 +383,7 @@
117 addr, direction, ethertype, vlan_tag, conj_ids):
118 self.driver._add_flow(**flow)
119
120- def update_flows_for_vlan(self, vlan_tag):
121+ def update_flows_for_vlan(self, vlan_tag, conj_id_to_remove=None):
122 """Install action=conjunction(conj_id, 1/2) flows,
123 which depend on IP addresses of remote_group_id.
124 """
125@@ -372,7 +395,7 @@
126 ethertype, sg_conj_id_map)
127 self._update_flows_for_vlan_subr(direction, ethertype, vlan_tag,
128 self.flow_state[vlan_tag][(direction, ethertype)],
129- addr_to_conj)
130+ addr_to_conj, conj_id_to_remove)
131 self.flow_state[vlan_tag][(direction, ethertype)] = addr_to_conj
132
133 def add(self, vlan_tag, sg_id, remote_sg_id, direction, ethertype,
134@@ -393,32 +416,43 @@
135 collections.defaultdict(set))
136 self.conj_ids[vlan_tag][(direction, ethertype)][remote_sg_id].add(
137 conj_id)
138+
139+ conj_id_tuple = (sg_id, remote_sg_id, direction, ethertype)
140+ self.conj_id_map.id_map_group[conj_id_tuple].add(conj_id)
141 return conj_id
142
143 def sg_removed(self, sg_id):
144 """Handle SG removal events.
145
146- Free all conj_ids associated with the sg_id and clean up
147+ Free all conj_ids associated with the sg_id removed and clean up
148 obsolete entries from the self.conj_ids map. Unlike the add
149 method, it also updates flows.
150+ If a SG is removed, both sg_id and remote_sg_id should be removed from
151+ the "vlan_conj_id_map".
152 """
153- id_list = self.conj_id_map.delete_sg(sg_id)
154+ id_set = self.conj_id_map.delete_sg(sg_id)
155 unused_dict = collections.defaultdict(set)
156- for remote_sg_id, conj_id in id_list:
157+ for remote_sg_id, conj_id in id_set:
158 unused_dict[remote_sg_id].add(conj_id)
159
160 for vlan_tag, vlan_conj_id_map in self.conj_ids.items():
161 update = False
162+ conj_id_to_remove = []
163 for sg_conj_id_map in vlan_conj_id_map.values():
164 for remote_sg_id, unused in unused_dict.items():
165 if (remote_sg_id in sg_conj_id_map and
166 sg_conj_id_map[remote_sg_id] & unused):
167+ if remote_sg_id == sg_id:
168+ conj_id_to_remove.append(
169+ sg_conj_id_map[remote_sg_id] & unused)
170 sg_conj_id_map[remote_sg_id] -= unused
171 if not sg_conj_id_map[remote_sg_id]:
172 del sg_conj_id_map[remote_sg_id]
173 update = True
174+
175 if update:
176- self.update_flows_for_vlan(vlan_tag)
177+ self.update_flows_for_vlan(vlan_tag,
178+ conj_id_to_remove=conj_id_to_remove)
179
180
181 class OVSFirewallDriver(firewall.FirewallDriver):
182@@ -499,7 +533,8 @@
183
184 def _delete_flows(self, **kwargs):
185 create_reg_numbers(kwargs)
186- if self._deferred:
187+ deferred = kwargs.pop('deferred', self._deferred)
188+ if deferred:
189 self.int_br.delete_flows(**kwargs)
190 else:
191 self.int_br.br.delete_flows(**kwargs)
192@@ -1416,20 +1451,31 @@
193 in_port=port.ofport)
194 self._delete_flows(reg_port=port.ofport)
195
196- def delete_flows_for_ip_addresses(
197- self, ip_addresses, direction, ethertype, vlan_tag):
198+ def delete_flows_for_flow_state(
199+ self, flow_state, addr_to_conj, direction, ethertype, vlan_tag):
200+ # Remove rules for deleted IPs and action=conjunction(conj_id, 1/2)
201+ removed_ips = set(flow_state.keys()) - set(addr_to_conj.keys())
202+ for removed_ip in removed_ips:
203+ conj_ids = flow_state[removed_ip]
204+ self.delete_flow_for_ip(removed_ip, direction, ethertype, vlan_tag,
205+ conj_ids)
206+
207 if not cfg.CONF.AGENT.explicitly_egress_direct:
208 return
209
210- for ip_addr in ip_addresses:
211+ for ip_addr in removed_ips:
212 # Generate deletion template with bogus conj_id.
213- flows = rules.create_flows_for_ip_address(
214- ip_addr, direction, ethertype, vlan_tag, [0])
215- for f in flows:
216- # The following del statements are partly for
217- # complying the OpenFlow spec. It forbids the use of
218- # these field in non-strict delete flow messages, and
219- # the actions field is bogus anyway.
220- del f['actions']
221- del f['priority']
222- self._delete_flows(**f)
223+ self.delete_flow_for_ip(ip_addr, direction, ethertype, vlan_tag,
224+ [0])
225+
226+ def delete_flow_for_ip(self, ip_address, direction, ethertype,
227+ vlan_tag, conj_ids):
228+ for flow in rules.create_flows_for_ip_address(
229+ ip_address, direction, ethertype, vlan_tag, conj_ids):
230+ # The following del statements are partly for
231+ # complying the OpenFlow spec. It forbids the use of
232+ # these field in non-strict delete flow messages, and
233+ # the actions field is bogus anyway.
234+ del flow['actions']
235+ del flow['priority']
236+ self._delete_flows(deferred=False, **flow)
237diff --git a/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py b/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py
238index 6aeb7a4..c1aed92 100644
239--- a/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py
240+++ b/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py
241@@ -282,23 +282,50 @@
242 constants.IPv6)
243
244 def test_delete_sg(self):
245- test_data = [('sg1', 'sg1'), ('sg1', 'sg2')]
246+ test_data = [
247+ # conj_id: 8
248+ ('sg1', 'sg1', constants.INGRESS_DIRECTION, constants.IPv6, 0),
249+ # conj_id: 10
250+ ('sg1', 'sg1', constants.INGRESS_DIRECTION, constants.IPv6, 1),
251+ # conj_id: 12
252+ ('sg1', 'sg1', constants.INGRESS_DIRECTION, constants.IPv6, 2),
253+ # conj_id: 16
254+ ('sg2', 'sg1', constants.EGRESS_DIRECTION, constants.IPv6, 0),
255+ # conj_id: 24
256+ ('sg1', 'sg3', constants.INGRESS_DIRECTION, constants.IPv6, 0),
257+ # conj_id: 36 (and 32 without priority offset, stored in id_map)
258+ ('sg3', 'sg4', constants.INGRESS_DIRECTION, constants.IPv4, 2),
259+ # conj_id: 40
260+ ('sg5', 'sg4', constants.EGRESS_DIRECTION, constants.IPv4, 0),
261+ ]
262
263 ids = []
264- for sg_id, remote_sg_id in test_data:
265- ids.append(self.conj_id_map.get_conj_id(
266- sg_id, remote_sg_id,
267- firewall.INGRESS_DIRECTION, constants.IPv6))
268+ conj_id_segment = set([]) # see ConjIPFlowManager.get_conj_id
269+ # This is similar to ConjIPFlowManager.add method
270+ for sg_id, rsg_id, direction, ip_version, prio_offset in test_data:
271+ conj_id_tuple = (sg_id, rsg_id, direction, ip_version)
272+ conj_id = self.conj_id_map.get_conj_id(*conj_id_tuple)
273+ conj_id_segment.add(conj_id)
274+ conj_id_plus_prio = conj_id + prio_offset * 2
275+ self.conj_id_map.id_map_group[conj_id_tuple].add(conj_id_plus_prio)
276+ ids.append(conj_id_plus_prio)
277
278 result = self.conj_id_map.delete_sg('sg1')
279- self.assertIn(('sg1', ids[0]), result)
280- self.assertIn(('sg2', ids[1]), result)
281- self.assertFalse(self.conj_id_map.id_map)
282+ self.assertEqual(
283+ {('sg3', 24), ('sg1', 12), ('sg1', 16), ('sg1', 8), ('sg1', 10)},
284+ result)
285+ result = self.conj_id_map.delete_sg('sg3')
286+ self.assertEqual({('sg4', 32), ('sg4', 36)}, result)
287+ result = self.conj_id_map.delete_sg('sg4')
288+ self.assertEqual({('sg4', 40)}, result)
289+ self.assertEqual({}, self.conj_id_map.id_map)
290+ self.assertEqual({}, self.conj_id_map.id_map_group)
291
292- reallocated = self.conj_id_map.get_conj_id(
293- 'sg-foo', 'sg-foo', firewall.INGRESS_DIRECTION,
294- constants.IPv6)
295- self.assertIn(reallocated, ids)
296+ reallocated = set([])
297+ for sg_id, rsg_id, direction, ip_version, _ in test_data:
298+ conj_id_tuple = (sg_id, rsg_id, direction, ip_version)
299+ reallocated.add(self.conj_id_map.get_conj_id(*conj_id_tuple))
300+ self.assertEqual(reallocated, conj_id_segment)
301
302
303 class TestConjIPFlowManager(base.BaseTestCase):
304@@ -363,7 +390,7 @@
305 dl_type=2048, nw_src='10.22.3.4/32', priority=73,
306 reg_net=self.vlan_tag, table=82)])
307
308- def test_sg_removed(self):
309+ def _sg_removed(self, sg_name):
310 with mock.patch.object(self.manager.conj_id_map,
311 'get_conj_id') as get_id_mock, \
312 mock.patch.object(self.manager.conj_id_map,
313@@ -376,11 +403,26 @@
314 firewall.INGRESS_DIRECTION, constants.IPv4)] = {
315 '10.22.3.4': [self.conj_id]}
316
317- self.manager.sg_removed('sg')
318+ self.manager.sg_removed(sg_name)
319+
320+ def test_sg_removed(self):
321+ self._sg_removed('sg')
322 self.driver._add_flow.assert_not_called()
323- self.driver.delete_flows_for_ip_addresses.assert_called_once_with(
324- {'10.22.3.4'}, firewall.INGRESS_DIRECTION, constants.IPv4,
325- self.vlan_tag)
326+ self.driver.delete_flows_for_flow_state.assert_called_once_with(
327+ {'10.22.3.4': [self.conj_id]}, {},
328+ constants.INGRESS_DIRECTION, constants.IPv4, self.vlan_tag)
329+ self.driver.delete_flow_for_ip.assert_not_called()
330+
331+ def test_remote_sg_removed(self):
332+ self._sg_removed('remote_id')
333+ self.driver._add_flow.assert_not_called()
334+ self.driver.delete_flows_for_flow_state.assert_called_once_with(
335+ {'10.22.3.4': [self.conj_id]}, {},
336+ constants.INGRESS_DIRECTION, constants.IPv4, self.vlan_tag)
337+ # "conj_id_to_remove" is populated with the remote_sg conj_id assigned,
338+ # "_update_flows_for_vlan_subr" will call "delete_flow_for_ip".
339+ self.driver.delete_flow_for_ip.assert_called_once_with(
340+ '10.22.3.4', 'ingress', 'IPv4', 100, {self.conj_id})
341
342
343 class FakeOVSPort(object):
344@@ -401,7 +443,6 @@
345 self.fake_ovs_port = FakeOVSPort('port', 1, '00:00:00:00:00:00')
346 self.mock_bridge.br.get_vif_port_by_id.return_value = \
347 self.fake_ovs_port
348- cfg.CONF.set_override('explicitly_egress_direct', True, 'AGENT')
349
350 def _prepare_security_group(self):
351 security_group_rules = [
352@@ -925,6 +966,43 @@
353 """Check that exception is not propagated outside."""
354 self.firewall.remove_trusted_ports(['port_id'])
355
356+ def _test_delete_flows_for_flow_state(self, addr_to_conj,
357+ explicitly_egress_direct=True):
358+ direction = 'one_direction'
359+ ethertype = 'ethertype'
360+ vlan_tag = 'taaag'
361+ with mock.patch.object(self.firewall, 'delete_flow_for_ip') as \
362+ mock_delete_flow_for_ip:
363+ flow_state = {'addr1': {8, 16, 24}, 'addr2': {32, 40}}
364+ cfg.CONF.set_override('explicitly_egress_direct',
365+ explicitly_egress_direct, 'AGENT')
366+ self.firewall.delete_flows_for_flow_state(
367+ flow_state, addr_to_conj, direction, ethertype, vlan_tag)
368+ calls = []
369+ for removed_ip in set(flow_state.keys()) - set(addr_to_conj.keys()):
370+ calls.append(mock.call(removed_ip, direction, ethertype, vlan_tag,
371+ flow_state[removed_ip]))
372+ if explicitly_egress_direct:
373+ calls.append(mock.call(removed_ip, direction, ethertype,
374+ vlan_tag, [0]))
375+ mock_delete_flow_for_ip.assert_has_calls(calls)
376+
377+ def test_delete_flows_for_flow_state_no_removed_ips_exp_egress(self):
378+ addr_to_conj = {'addr1': {8, 16, 24}, 'addr2': {32, 40}}
379+ self._test_delete_flows_for_flow_state(addr_to_conj)
380+
381+ def test_delete_flows_for_flow_state_no_removed_ips_no_exp_egress(self):
382+ addr_to_conj = {'addr1': {8, 16, 24}, 'addr2': {32, 40}}
383+ self._test_delete_flows_for_flow_state(addr_to_conj, False)
384+
385+ def test_delete_flows_for_flow_state_removed_ips_exp_egress(self):
386+ addr_to_conj = {'addr2': {32, 40}}
387+ self._test_delete_flows_for_flow_state(addr_to_conj)
388+
389+ def test_delete_flows_for_flow_state_removed_ips_no_exp_egress(self):
390+ addr_to_conj = {'addr1': {8, 16, 24}}
391+ self._test_delete_flows_for_flow_state(addr_to_conj, False)
392+
393
394 class TestCookieContext(base.BaseTestCase):
395 def setUp(self):
diff --git a/debian/patches/series b/debian/patches/series
index 96882c3..e65079e 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,3 +1,4 @@
1skip-iptest.patch1skip-iptest.patch
2flake8-legacy.patch2flake8-legacy.patch
3Ensure-fip-ip-rules-deleted-when-fip-removed.patch3Ensure-fip-ip-rules-deleted-when-fip-removed.patch
4lp1881157-remote-sg-is-left-behind.patch

Subscribers

People subscribed via source and target branches