Merge ~fnordahl/ubuntu/+source/python-openstackclient:master into ~ubuntu-openstack-dev/ubuntu/+source/python-openstackclient:master
- Git
- lp:~fnordahl/ubuntu/+source/python-openstackclient
- master
- Merge into master
Proposed by
Frode Nordahl
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | d2a74e9a52baf9c6d96eca55541f0223bf47be80 | ||||
Proposed branch: | ~fnordahl/ubuntu/+source/python-openstackclient:master | ||||
Merge into: | ~ubuntu-openstack-dev/ubuntu/+source/python-openstackclient:master | ||||
Diff against target: |
1802 lines (+1764/-0) 6 files modified
debian/changelog (+7/-0) debian/patches/lp-2002687-1-Parse-external-gateway-argument-in-separate-helper.patch (+109/-0) debian/patches/lp-2002687-2-router-Use-plural-form-for-storage-of-fixed_ip-argum.patch (+130/-0) debian/patches/lp-2002687-3-Add-support-for-managing-external-gateways.patch (+1294/-0) debian/patches/lp-2002687-4-Add-router-default-route-BFD-ECMP-options.patch (+220/-0) debian/patches/series (+4/-0) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Page | Pending | ||
Review via email: mp+463871@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/debian/changelog b/debian/changelog |
2 | index 2ecccc2..b6da2bb 100644 |
3 | --- a/debian/changelog |
4 | +++ b/debian/changelog |
5 | @@ -1,3 +1,10 @@ |
6 | +python-openstackclient (6.6.0-0ubuntu2) UNRELEASED; urgency=medium |
7 | + |
8 | + * d/p/lp-2002687-*: Add support for managing multiple gateways and |
9 | + BFD/ECMP options (LP: #2002687). |
10 | + |
11 | + -- Frode Nordahl <fnordahl@ubuntu.com> Tue, 09 Apr 2024 06:29:16 +0000 |
12 | + |
13 | python-openstackclient (6.6.0-0ubuntu1) noble; urgency=medium |
14 | |
15 | [ Corey Bryant ] |
16 | diff --git a/debian/patches/lp-2002687-1-Parse-external-gateway-argument-in-separate-helper.patch b/debian/patches/lp-2002687-1-Parse-external-gateway-argument-in-separate-helper.patch |
17 | new file mode 100644 |
18 | index 0000000..a2a4365 |
19 | --- /dev/null |
20 | +++ b/debian/patches/lp-2002687-1-Parse-external-gateway-argument-in-separate-helper.patch |
21 | @@ -0,0 +1,109 @@ |
22 | +From 3915baa0b7996fd2fe34f44239bccd9b60836126 Mon Sep 17 00:00:00 2001 |
23 | +From: Frode Nordahl <fnordahl@ubuntu.com> |
24 | +Date: Tue, 5 Mar 2024 11:15:23 +0100 |
25 | +Subject: [PATCH 1/4] Parse external-gateway argument in separate helper |
26 | + |
27 | +From: Frode Nordahl <frode.nordahl@canonical.com> |
28 | + |
29 | +This is to prepare for subsequent patches that will add support |
30 | +for managing multiple gateways. |
31 | + |
32 | +Related-Bug: #2002687 |
33 | +Change-Id: Ic088dca0b7cd83bd7568d775b4e70285ce72411d |
34 | +Signed-off-by: Frode Nordahl <frode.nordahl@canonical.com> |
35 | +(cherry picked from commit f696aee81d8b20ffdaa11c786c7e55cd7aa9cbb8) |
36 | + |
37 | +Origin: upstream, https://opendev.org/openstack/python-openstackclient/commit/f696aee81d8b20ffdaa11c786c7e55cd7aa9cbb8 |
38 | +Bug: https://launchpad.net/bugs/2002687 |
39 | +Description: The main feature went into OpenStack Neutron at Caracal, and due |
40 | + to review capacity the client patches did unfortunately not make it. Enable |
41 | + the feature for our users while we await the next python-openstackclient |
42 | + release. |
43 | + |
44 | +--- |
45 | + openstackclient/network/v2/router.py | 62 ++++++++++++++++------------ |
46 | + 1 file changed, 36 insertions(+), 26 deletions(-) |
47 | + |
48 | +diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py |
49 | +index fdb7102b..b4430153 100644 |
50 | +--- a/openstackclient/network/v2/router.py |
51 | ++++ b/openstackclient/network/v2/router.py |
52 | +@@ -85,6 +85,39 @@ def _get_columns(item): |
53 | + ) |
54 | + |
55 | + |
56 | ++def _get_external_gateway_attrs(client_manager, parsed_args): |
57 | ++ attrs = {} |
58 | ++ |
59 | ++ if parsed_args.external_gateway: |
60 | ++ gateway_info = {} |
61 | ++ n_client = client_manager.network |
62 | ++ network = n_client.find_network( |
63 | ++ parsed_args.external_gateway, ignore_missing=False |
64 | ++ ) |
65 | ++ gateway_info['network_id'] = network.id |
66 | ++ if parsed_args.disable_snat: |
67 | ++ gateway_info['enable_snat'] = False |
68 | ++ if parsed_args.enable_snat: |
69 | ++ gateway_info['enable_snat'] = True |
70 | ++ if parsed_args.fixed_ip: |
71 | ++ ips = [] |
72 | ++ for ip_spec in parsed_args.fixed_ip: |
73 | ++ if ip_spec.get('subnet', False): |
74 | ++ subnet_name_id = ip_spec.pop('subnet') |
75 | ++ if subnet_name_id: |
76 | ++ subnet = n_client.find_subnet( |
77 | ++ subnet_name_id, ignore_missing=False |
78 | ++ ) |
79 | ++ ip_spec['subnet_id'] = subnet.id |
80 | ++ if ip_spec.get('ip-address', False): |
81 | ++ ip_spec['ip_address'] = ip_spec.pop('ip-address') |
82 | ++ ips.append(ip_spec) |
83 | ++ gateway_info['external_fixed_ips'] = ips |
84 | ++ attrs['external_gateway_info'] = gateway_info |
85 | ++ |
86 | ++ return attrs |
87 | ++ |
88 | ++ |
89 | + def _get_attrs(client_manager, parsed_args): |
90 | + attrs = {} |
91 | + if parsed_args.name is not None: |
92 | +@@ -113,32 +146,9 @@ def _get_attrs(client_manager, parsed_args): |
93 | + parsed_args.project_domain, |
94 | + ).id |
95 | + attrs['project_id'] = project_id |
96 | +- if parsed_args.external_gateway: |
97 | +- gateway_info = {} |
98 | +- n_client = client_manager.network |
99 | +- network = n_client.find_network( |
100 | +- parsed_args.external_gateway, ignore_missing=False |
101 | +- ) |
102 | +- gateway_info['network_id'] = network.id |
103 | +- if parsed_args.disable_snat: |
104 | +- gateway_info['enable_snat'] = False |
105 | +- if parsed_args.enable_snat: |
106 | +- gateway_info['enable_snat'] = True |
107 | +- if parsed_args.fixed_ip: |
108 | +- ips = [] |
109 | +- for ip_spec in parsed_args.fixed_ip: |
110 | +- if ip_spec.get('subnet', False): |
111 | +- subnet_name_id = ip_spec.pop('subnet') |
112 | +- if subnet_name_id: |
113 | +- subnet = n_client.find_subnet( |
114 | +- subnet_name_id, ignore_missing=False |
115 | +- ) |
116 | +- ip_spec['subnet_id'] = subnet.id |
117 | +- if ip_spec.get('ip-address', False): |
118 | +- ip_spec['ip_address'] = ip_spec.pop('ip-address') |
119 | +- ips.append(ip_spec) |
120 | +- gateway_info['external_fixed_ips'] = ips |
121 | +- attrs['external_gateway_info'] = gateway_info |
122 | ++ |
123 | ++ attrs.update(_get_external_gateway_attrs(client_manager, parsed_args)) |
124 | ++ |
125 | + # "router set" command doesn't support setting flavor_id. |
126 | + if 'flavor_id' in parsed_args and parsed_args.flavor_id is not None: |
127 | + attrs['flavor_id'] = parsed_args.flavor_id |
128 | +-- |
129 | +2.43.0 |
130 | + |
131 | diff --git a/debian/patches/lp-2002687-2-router-Use-plural-form-for-storage-of-fixed_ip-argum.patch b/debian/patches/lp-2002687-2-router-Use-plural-form-for-storage-of-fixed_ip-argum.patch |
132 | new file mode 100644 |
133 | index 0000000..cfef2e3 |
134 | --- /dev/null |
135 | +++ b/debian/patches/lp-2002687-2-router-Use-plural-form-for-storage-of-fixed_ip-argum.patch |
136 | @@ -0,0 +1,130 @@ |
137 | +From c4c86884c5c706dcf148a30f2b24ad07db4b44a5 Mon Sep 17 00:00:00 2001 |
138 | +From: Frode Nordahl <fnordahl@ubuntu.com> |
139 | +Date: Tue, 5 Mar 2024 15:56:49 +0100 |
140 | +Subject: [PATCH 2/4] router: Use plural form for storage of ``--fixed_ip`` |
141 | + argument |
142 | + |
143 | +From: Frode Nordahl <frode.nordahl@canonical.com> |
144 | + |
145 | +The variable already takes multiple values, let's make it obvious |
146 | +just by reading the code. |
147 | + |
148 | +Related-Bug: #2002687 |
149 | +Change-Id: I294ee710d989d7a3a54331fca424e84708a2faab |
150 | +Signed-off-by: Frode Nordahl <frode.nordahl@canonical.com> |
151 | +(cherry picked from commit 58ad3cefa7870320d9d9835b202f7b9661b1d825) |
152 | + |
153 | +Origin: upstream, https://opendev.org/openstack/python-openstackclient/commit/58ad3cefa7870320d9d9835b202f7b9661b1d825 |
154 | +Bug: https://launchpad.net/bugs/2002687 |
155 | +Description: The main feature went into OpenStack Neutron at Caracal, and due |
156 | + to review capacity the client patches did unfortunately not make it. Enable |
157 | + the feature for our users while we await the next python-openstackclient |
158 | + release. |
159 | + |
160 | +--- |
161 | + openstackclient/network/v2/router.py | 10 ++++++---- |
162 | + openstackclient/tests/unit/network/v2/test_router.py | 10 +++++----- |
163 | + 2 files changed, 11 insertions(+), 9 deletions(-) |
164 | + |
165 | +diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py |
166 | +index b4430153..21170ffa 100644 |
167 | +--- a/openstackclient/network/v2/router.py |
168 | ++++ b/openstackclient/network/v2/router.py |
169 | +@@ -99,9 +99,9 @@ def _get_external_gateway_attrs(client_manager, parsed_args): |
170 | + gateway_info['enable_snat'] = False |
171 | + if parsed_args.enable_snat: |
172 | + gateway_info['enable_snat'] = True |
173 | +- if parsed_args.fixed_ip: |
174 | ++ if parsed_args.fixed_ips: |
175 | + ips = [] |
176 | +- for ip_spec in parsed_args.fixed_ip: |
177 | ++ for ip_spec in parsed_args.fixed_ips: |
178 | + if ip_spec.get('subnet', False): |
179 | + subnet_name_id = ip_spec.pop('subnet') |
180 | + if subnet_name_id: |
181 | +@@ -379,6 +379,7 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): |
182 | + metavar='subnet=<subnet>,ip-address=<ip-address>', |
183 | + action=parseractions.MultiKeyValueAction, |
184 | + optional_keys=['subnet', 'ip-address'], |
185 | ++ dest='fixed_ips', |
186 | + help=_( |
187 | + "Desired IP and/or subnet (name or ID) " |
188 | + "on external gateway: " |
189 | +@@ -449,7 +450,7 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): |
190 | + if ( |
191 | + parsed_args.disable_snat |
192 | + or parsed_args.enable_snat |
193 | +- or parsed_args.fixed_ip |
194 | ++ or parsed_args.fixed_ips |
195 | + ) and not parsed_args.external_gateway: |
196 | + msg = _( |
197 | + "You must specify '--external-gateway' in order " |
198 | +@@ -797,6 +798,7 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
199 | + metavar='subnet=<subnet>,ip-address=<ip-address>', |
200 | + action=parseractions.MultiKeyValueAction, |
201 | + optional_keys=['subnet', 'ip-address'], |
202 | ++ dest='fixed_ips', |
203 | + help=_( |
204 | + "Desired IP and/or subnet (name or ID) " |
205 | + "on external gateway: " |
206 | +@@ -870,7 +872,7 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
207 | + if ( |
208 | + parsed_args.disable_snat |
209 | + or parsed_args.enable_snat |
210 | +- or parsed_args.fixed_ip |
211 | ++ or parsed_args.fixed_ips |
212 | + ) and not parsed_args.external_gateway: |
213 | + msg = _( |
214 | + "You must specify '--external-gateway' in order " |
215 | +diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py |
216 | +index 9935db4d..fb12e3f0 100644 |
217 | +--- a/openstackclient/tests/unit/network/v2/test_router.py |
218 | ++++ b/openstackclient/tests/unit/network/v2/test_router.py |
219 | +@@ -230,7 +230,7 @@ class TestCreateRouter(TestRouter): |
220 | + ('ha', False), |
221 | + ('external_gateway', _network.name), |
222 | + ('enable_snat', True), |
223 | +- ('fixed_ip', [{'ip-address': '2001:db8::1'}]), |
224 | ++ ('fixed_ips', [{'ip-address': '2001:db8::1'}]), |
225 | + ] |
226 | + parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
227 | + |
228 | +@@ -1297,7 +1297,7 @@ class TestSetRouter(TestRouter): |
229 | + self._router.id, |
230 | + ] |
231 | + verifylist = [ |
232 | +- ('fixed_ip', [{'subnet': "'abc'"}]), |
233 | ++ ('fixed_ips', [{'subnet': "'abc'"}]), |
234 | + ('router', self._router.id), |
235 | + ] |
236 | + parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
237 | +@@ -1336,7 +1336,7 @@ class TestSetRouter(TestRouter): |
238 | + verifylist = [ |
239 | + ('router', self._router.id), |
240 | + ('external_gateway', self._network.id), |
241 | +- ('fixed_ip', [{'subnet': "'abc'"}]), |
242 | ++ ('fixed_ips', [{'subnet': "'abc'"}]), |
243 | + ('enable_snat', True), |
244 | + ] |
245 | + parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
246 | +@@ -1370,7 +1370,7 @@ class TestSetRouter(TestRouter): |
247 | + verifylist = [ |
248 | + ('router', self._router.id), |
249 | + ('external_gateway', self._network.id), |
250 | +- ('fixed_ip', [{'ip-address': "10.0.1.1"}]), |
251 | ++ ('fixed_ips', [{'ip-address': "10.0.1.1"}]), |
252 | + ('enable_snat', True), |
253 | + ] |
254 | + parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
255 | +@@ -1404,7 +1404,7 @@ class TestSetRouter(TestRouter): |
256 | + verifylist = [ |
257 | + ('router', self._router.id), |
258 | + ('external_gateway', self._network.id), |
259 | +- ('fixed_ip', [{'subnet': "'abc'", 'ip-address': "10.0.1.1"}]), |
260 | ++ ('fixed_ips', [{'subnet': "'abc'", 'ip-address': "10.0.1.1"}]), |
261 | + ('enable_snat', True), |
262 | + ] |
263 | + parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
264 | +-- |
265 | +2.43.0 |
266 | + |
267 | diff --git a/debian/patches/lp-2002687-3-Add-support-for-managing-external-gateways.patch b/debian/patches/lp-2002687-3-Add-support-for-managing-external-gateways.patch |
268 | new file mode 100644 |
269 | index 0000000..e977a4c |
270 | --- /dev/null |
271 | +++ b/debian/patches/lp-2002687-3-Add-support-for-managing-external-gateways.patch |
272 | @@ -0,0 +1,1294 @@ |
273 | +From f60a7cc006632b0c6478f4a0dfcb8ed217e960ae Mon Sep 17 00:00:00 2001 |
274 | +From: Frode Nordahl <fnordahl@ubuntu.com> |
275 | +Date: Fri, 7 Jul 2023 19:08:33 +0300 |
276 | +Subject: [PATCH 3/4] Add support for managing external gateways |
277 | + |
278 | +From: Dmitrii Shcherbakov <dmitrii.shcherbakov@canonical.com> |
279 | + |
280 | +This change implements the logic to call the new API for managing |
281 | +external gateways. |
282 | + |
283 | +Relevant Neutron core change: |
284 | +https://review.opendev.org/c/openstack/neutron/+/873593 |
285 | + |
286 | +Co-Authored-by: Frode Nordahl <frode.nordahl@canonical.com> |
287 | +Related-Bug: #2002687 |
288 | +Change-Id: Ib45f30f552934a0a5c035c3b7fadfc0d522219ba |
289 | +(cherry picked from commit 16c695045c5618a733574fd2a5558d672a2df553) |
290 | +Conflicts: |
291 | + setup.cfg |
292 | + |
293 | +Origin: upstream, https://opendev.org/openstack/python-openstackclient/commit/16c695045c5618a733574fd2a5558d672a2df553 |
294 | +Bug: https://launchpad.net/bugs/2002687 |
295 | +Description: The main feature went into OpenStack Neutron at Caracal, and due |
296 | + to review capacity the client patches did unfortunately not make it. Enable |
297 | + the feature for our users while we await the next python-openstackclient |
298 | + release. |
299 | + |
300 | +--- |
301 | + openstackclient/network/v2/router.py | 313 ++++++++- |
302 | + .../tests/unit/network/v2/test_router.py | 616 +++++++++++++++++- |
303 | + setup.cfg | 2 + |
304 | + 3 files changed, 885 insertions(+), 46 deletions(-) |
305 | + |
306 | +diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py |
307 | +index 21170ffa..52b7d25f 100644 |
308 | +--- a/openstackclient/network/v2/router.py |
309 | ++++ b/openstackclient/network/v2/router.py |
310 | +@@ -13,6 +13,7 @@ |
311 | + |
312 | + """Router action implementations""" |
313 | + |
314 | ++import collections |
315 | + import copy |
316 | + import json |
317 | + import logging |
318 | +@@ -85,23 +86,67 @@ def _get_columns(item): |
319 | + ) |
320 | + |
321 | + |
322 | ++def is_multiple_gateways_supported(n_client): |
323 | ++ return n_client.find_extension("external-gateway-multihoming") is not None |
324 | ++ |
325 | ++ |
326 | ++def _passed_multiple_gateways(extension_supported, external_gateways): |
327 | ++ passed_multiple_gws = len(external_gateways) > 1 |
328 | ++ if passed_multiple_gws and not extension_supported: |
329 | ++ msg = _( |
330 | ++ 'Supplying --external-gateway option multiple times is not ' |
331 | ++ 'supported due to the lack of external-gateway-multihoming ' |
332 | ++ 'extension at the Neutron side.' |
333 | ++ ) |
334 | ++ raise exceptions.CommandError(msg) |
335 | ++ return passed_multiple_gws |
336 | ++ |
337 | ++ |
338 | + def _get_external_gateway_attrs(client_manager, parsed_args): |
339 | + attrs = {} |
340 | + |
341 | +- if parsed_args.external_gateway: |
342 | +- gateway_info = {} |
343 | ++ if parsed_args.external_gateways: |
344 | ++ external_gateways: collections.defaultdict[ |
345 | ++ str, list[dict] |
346 | ++ ] = collections.defaultdict(list) |
347 | + n_client = client_manager.network |
348 | +- network = n_client.find_network( |
349 | +- parsed_args.external_gateway, ignore_missing=False |
350 | +- ) |
351 | +- gateway_info['network_id'] = network.id |
352 | +- if parsed_args.disable_snat: |
353 | +- gateway_info['enable_snat'] = False |
354 | +- if parsed_args.enable_snat: |
355 | +- gateway_info['enable_snat'] = True |
356 | ++ first_network_id = None |
357 | ++ |
358 | ++ for gw_net_name_or_id in parsed_args.external_gateways: |
359 | ++ gateway_info = {} |
360 | ++ gw_net = n_client.find_network( |
361 | ++ gw_net_name_or_id, ignore_missing=False |
362 | ++ ) |
363 | ++ if first_network_id is None: |
364 | ++ first_network_id = gw_net.id |
365 | ++ gateway_info['network_id'] = gw_net.id |
366 | ++ if 'disable_snat' in parsed_args and parsed_args.disable_snat: |
367 | ++ gateway_info['enable_snat'] = False |
368 | ++ if 'enable_snat' in parsed_args and parsed_args.enable_snat: |
369 | ++ gateway_info['enable_snat'] = True |
370 | ++ |
371 | ++ # This option was added before multiple gateways were supported, so |
372 | ++ # it does not have a per-gateway port granularity so just pass it |
373 | ++ # along in gw info in case it is specified. |
374 | ++ if 'qos_policy' in parsed_args and parsed_args.qos_policy: |
375 | ++ qos_id = n_client.find_qos_policy( |
376 | ++ parsed_args.qos_policy, ignore_missing=False |
377 | ++ ).id |
378 | ++ gateway_info['qos_policy_id'] = qos_id |
379 | ++ if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy: |
380 | ++ gateway_info['qos_policy_id'] = None |
381 | ++ |
382 | ++ external_gateways[gw_net.id].append(gateway_info) |
383 | ++ |
384 | ++ multiple_gws_supported = is_multiple_gateways_supported(n_client) |
385 | ++ # Parse the external fixed IP specs and match them to specific gateway |
386 | ++ # ports if needed. |
387 | + if parsed_args.fixed_ips: |
388 | +- ips = [] |
389 | + for ip_spec in parsed_args.fixed_ips: |
390 | ++ # If there is only one gateway, this value will represent the |
391 | ++ # network ID for it, otherwise it will be overridden. |
392 | ++ ip_net_id = first_network_id |
393 | ++ |
394 | + if ip_spec.get('subnet', False): |
395 | + subnet_name_id = ip_spec.pop('subnet') |
396 | + if subnet_name_id: |
397 | +@@ -109,12 +154,45 @@ def _get_external_gateway_attrs(client_manager, parsed_args): |
398 | + subnet_name_id, ignore_missing=False |
399 | + ) |
400 | + ip_spec['subnet_id'] = subnet.id |
401 | ++ ip_net_id = subnet.network_id |
402 | + if ip_spec.get('ip-address', False): |
403 | + ip_spec['ip_address'] = ip_spec.pop('ip-address') |
404 | +- ips.append(ip_spec) |
405 | +- gateway_info['external_fixed_ips'] = ips |
406 | +- attrs['external_gateway_info'] = gateway_info |
407 | +- |
408 | ++ # Finally, add an ip_spec to the specific gateway identified |
409 | ++ # by a network from the spec. |
410 | ++ if ( |
411 | ++ 'subnet_id' in ip_spec |
412 | ++ and ip_net_id not in external_gateways |
413 | ++ ): |
414 | ++ msg = _( |
415 | ++ 'Subnet %s does not belong to any of the networks ' |
416 | ++ 'provided for --external-gateway.' |
417 | ++ ) % (ip_spec['subnet_id']) |
418 | ++ raise exceptions.CommandError(msg) |
419 | ++ for gw_info in external_gateways[ip_net_id]: |
420 | ++ if 'external_fixed_ips' not in gw_info: |
421 | ++ gw_info['external_fixed_ips'] = [ip_spec] |
422 | ++ break |
423 | ++ else: |
424 | ++ # The end user has requested more fixed IPs than there are |
425 | ++ # gateways, add multiple fixed IPs to single gateway to |
426 | ++ # retain current behavior. |
427 | ++ for gw_info in external_gateways[ip_net_id]: |
428 | ++ gw_info['external_fixed_ips'].append(ip_spec) |
429 | ++ break |
430 | ++ |
431 | ++ # Use the newer API whenever it is supported regardless of whether one |
432 | ++ # or multiple gateways are passed as arguments. |
433 | ++ if multiple_gws_supported: |
434 | ++ gateway_list = [] |
435 | ++ # Now merge the per-network-id lists of external gateway info |
436 | ++ # dicts into one list. |
437 | ++ for gw_info_list in external_gateways.values(): |
438 | ++ gateway_list.extend(gw_info_list) |
439 | ++ attrs['external_gateways'] = gateway_list |
440 | ++ else: |
441 | ++ attrs['external_gateway_info'] = external_gateways[ |
442 | ++ first_network_id |
443 | ++ ][0] |
444 | + return attrs |
445 | + |
446 | + |
447 | +@@ -372,7 +450,13 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): |
448 | + parser.add_argument( |
449 | + '--external-gateway', |
450 | + metavar="<network>", |
451 | +- help=_("External Network used as router's gateway (name or ID)"), |
452 | ++ action='append', |
453 | ++ help=_( |
454 | ++ "External Network used as router's gateway (name or ID). " |
455 | ++ "(repeat option to set multiple gateways per router " |
456 | ++ "if the L3 service plugin in use supports it)." |
457 | ++ ), |
458 | ++ dest='external_gateways', |
459 | + ) |
460 | + parser.add_argument( |
461 | + '--fixed-ip', |
462 | +@@ -384,7 +468,7 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): |
463 | + "Desired IP and/or subnet (name or ID) " |
464 | + "on external gateway: " |
465 | + "subnet=<subnet>,ip-address=<ip-address> " |
466 | +- "(repeat option to set multiple fixed IP addresses)" |
467 | ++ "(repeat option to set multiple fixed IP addresses)." |
468 | + ), |
469 | + ) |
470 | + snat_group = parser.add_mutually_exclusive_group() |
471 | +@@ -433,7 +517,7 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): |
472 | + self._parse_extra_properties(parsed_args.extra_properties) |
473 | + ) |
474 | + |
475 | +- if parsed_args.enable_ndp_proxy and not parsed_args.external_gateway: |
476 | ++ if parsed_args.enable_ndp_proxy and not parsed_args.external_gateways: |
477 | + msg = _( |
478 | + "You must specify '--external-gateway' in order " |
479 | + "to enable router's NDP proxy" |
480 | +@@ -443,15 +527,24 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): |
481 | + if parsed_args.enable_ndp_proxy is not None: |
482 | + attrs['enable_ndp_proxy'] = parsed_args.enable_ndp_proxy |
483 | + |
484 | ++ external_gateways = attrs.pop('external_gateways', None) |
485 | + obj = client.create_router(**attrs) |
486 | + # tags cannot be set when created, so tags need to be set later. |
487 | + _tag.update_tags_for_set(client, obj, parsed_args) |
488 | + |
489 | ++ # If the multiple external gateways API is intended to be used, |
490 | ++ # do a separate API call to set the desired external gateways as the |
491 | ++ # router creation API supports adding only one. |
492 | ++ if external_gateways: |
493 | ++ client.update_external_gateways( |
494 | ++ obj, body={'router': {'external_gateways': external_gateways}} |
495 | ++ ) |
496 | ++ |
497 | + if ( |
498 | + parsed_args.disable_snat |
499 | + or parsed_args.enable_snat |
500 | + or parsed_args.fixed_ips |
501 | +- ) and not parsed_args.external_gateway: |
502 | ++ ) and not parsed_args.external_gateways: |
503 | + msg = _( |
504 | + "You must specify '--external-gateway' in order " |
505 | + "to specify SNAT or fixed-ip values" |
506 | +@@ -791,7 +884,13 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
507 | + parser.add_argument( |
508 | + '--external-gateway', |
509 | + metavar="<network>", |
510 | +- help=_("External Network used as router's gateway (name or ID)"), |
511 | ++ action='append', |
512 | ++ help=_( |
513 | ++ "External Network used as router's gateway (name or ID). " |
514 | ++ "(repeat option to set multiple gateways per router " |
515 | ++ "if the L3 service plugin in use supports it)." |
516 | ++ ), |
517 | ++ dest='external_gateways', |
518 | + ) |
519 | + parser.add_argument( |
520 | + '--fixed-ip', |
521 | +@@ -803,7 +902,7 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
522 | + "Desired IP and/or subnet (name or ID) " |
523 | + "on external gateway: " |
524 | + "subnet=<subnet>,ip-address=<ip-address> " |
525 | +- "(repeat option to set multiple fixed IP addresses)" |
526 | ++ "(repeat option to set multiple fixed IP addresses)." |
527 | + ), |
528 | + ) |
529 | + snat_group = parser.add_mutually_exclusive_group() |
530 | +@@ -873,7 +972,7 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
531 | + parsed_args.disable_snat |
532 | + or parsed_args.enable_snat |
533 | + or parsed_args.fixed_ips |
534 | +- ) and not parsed_args.external_gateway: |
535 | ++ ) and not parsed_args.external_gateways: |
536 | + msg = _( |
537 | + "You must specify '--external-gateway' in order " |
538 | + "to update the SNAT or fixed-ip values" |
539 | +@@ -882,7 +981,7 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
540 | + |
541 | + if ( |
542 | + parsed_args.qos_policy or parsed_args.no_qos_policy |
543 | +- ) and not parsed_args.external_gateway: |
544 | ++ ) and not parsed_args.external_gateways: |
545 | + try: |
546 | + original_net_id = obj.external_gateway_info['network_id'] |
547 | + except (KeyError, TypeError): |
548 | +@@ -893,17 +992,21 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
549 | + ) |
550 | + raise exceptions.CommandError(msg) |
551 | + else: |
552 | +- if not attrs.get('external_gateway_info'): |
553 | ++ if not attrs.get('external_gateway_info') and not attrs.get( |
554 | ++ 'external_gateways' |
555 | ++ ): |
556 | + attrs['external_gateway_info'] = {} |
557 | + attrs['external_gateway_info']['network_id'] = original_net_id |
558 | + if parsed_args.qos_policy: |
559 | + check_qos_id = client.find_qos_policy( |
560 | + parsed_args.qos_policy, ignore_missing=False |
561 | + ).id |
562 | +- attrs['external_gateway_info']['qos_policy_id'] = check_qos_id |
563 | ++ if not attrs.get('external_gateways'): |
564 | ++ attrs['external_gateway_info']['qos_policy_id'] = check_qos_id |
565 | + |
566 | + if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy: |
567 | +- attrs['external_gateway_info']['qos_policy_id'] = None |
568 | ++ if not attrs.get('external_gateways'): |
569 | ++ attrs['external_gateway_info']['qos_policy_id'] = None |
570 | + |
571 | + attrs.update( |
572 | + self._parse_extra_properties(parsed_args.extra_properties) |
573 | +@@ -913,7 +1016,16 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
574 | + attrs['enable_ndp_proxy'] = parsed_args.enable_ndp_proxy |
575 | + |
576 | + if attrs: |
577 | ++ external_gateways = attrs.pop('external_gateways', None) |
578 | + client.update_router(obj, **attrs) |
579 | ++ # If the multiple external gateways API is intended to be used, |
580 | ++ # do a separate API call to set external gateways. |
581 | ++ if external_gateways: |
582 | ++ client.update_external_gateways( |
583 | ++ obj, |
584 | ++ body={'router': {'external_gateways': external_gateways}}, |
585 | ++ ) |
586 | ++ |
587 | + # tags is a subresource and it needs to be updated separately. |
588 | + _tag.update_tags_for_set(client, obj, parsed_args) |
589 | + |
590 | +@@ -973,11 +1085,15 @@ class UnsetRouter(common.NeutronUnsetCommandWithExtraArgs): |
591 | + "(repeat option to unset multiple routes)" |
592 | + ), |
593 | + ) |
594 | ++ # NOTE(dmitriis): This was not extended to support selective removal |
595 | ++ # of external gateways due to a cpython bug in argparse: |
596 | ++ # https://github.com/python/cpython/issues/53584 |
597 | + parser.add_argument( |
598 | + '--external-gateway', |
599 | + action='store_true', |
600 | + default=False, |
601 | + help=_("Remove external gateway information from the router"), |
602 | ++ dest='external_gateways', |
603 | + ) |
604 | + parser.add_argument( |
605 | + '--qos-policy', |
606 | +@@ -1024,7 +1140,7 @@ class UnsetRouter(common.NeutronUnsetCommandWithExtraArgs): |
607 | + 'qos_policy_id': None, |
608 | + } |
609 | + |
610 | +- if parsed_args.external_gateway: |
611 | ++ if parsed_args.external_gateways: |
612 | + attrs['external_gateway_info'] = {} |
613 | + |
614 | + attrs.update( |
615 | +@@ -1032,6 +1148,149 @@ class UnsetRouter(common.NeutronUnsetCommandWithExtraArgs): |
616 | + ) |
617 | + |
618 | + if attrs: |
619 | ++ # If removing multiple gateways per router are supported, |
620 | ++ # use the relevant API to remove them all. |
621 | ++ if is_multiple_gateways_supported(client): |
622 | ++ client.remove_external_gateways( |
623 | ++ obj, |
624 | ++ body={'router': {'external_gateways': {}}}, |
625 | ++ ) |
626 | ++ |
627 | + client.update_router(obj, **attrs) |
628 | + # tags is a subresource and it needs to be updated separately. |
629 | + _tag.update_tags_for_unset(client, obj, parsed_args) |
630 | ++ |
631 | ++ |
632 | ++class AddGatewayToRouter(command.ShowOne): |
633 | ++ _description = _("Add router gateway") |
634 | ++ |
635 | ++ def get_parser(self, prog_name): |
636 | ++ parser = super().get_parser(prog_name) |
637 | ++ parser.add_argument( |
638 | ++ 'router', |
639 | ++ metavar="<router>", |
640 | ++ help=_("Router to modify (name or ID)."), |
641 | ++ ) |
642 | ++ parser.add_argument( |
643 | ++ metavar="<network>", |
644 | ++ help=_( |
645 | ++ "External Network to a attach a router gateway to (name or " |
646 | ++ "ID)." |
647 | ++ ), |
648 | ++ dest='external_gateways', |
649 | ++ # The argument is stored in a list in order to reuse the |
650 | ++ # common attribute parsing code. |
651 | ++ nargs=1, |
652 | ++ ) |
653 | ++ parser.add_argument( |
654 | ++ '--fixed-ip', |
655 | ++ metavar='subnet=<subnet>,ip-address=<ip-address>', |
656 | ++ action=parseractions.MultiKeyValueAction, |
657 | ++ optional_keys=['subnet', 'ip-address'], |
658 | ++ dest='fixed_ips', |
659 | ++ help=_( |
660 | ++ "Desired IP and/or subnet (name or ID) " |
661 | ++ "on external gateway: " |
662 | ++ "subnet=<subnet>,ip-address=<ip-address> " |
663 | ++ "(repeat option to set multiple fixed IP addresses)." |
664 | ++ ), |
665 | ++ ) |
666 | ++ return parser |
667 | ++ |
668 | ++ def take_action(self, parsed_args): |
669 | ++ client = self.app.client_manager.network |
670 | ++ if not is_multiple_gateways_supported(client): |
671 | ++ msg = _( |
672 | ++ 'The external-gateway-multihoming extension is not enabled at ' |
673 | ++ 'the Neutron side.' |
674 | ++ ) |
675 | ++ raise exceptions.CommandError(msg) |
676 | ++ |
677 | ++ router_obj = client.find_router( |
678 | ++ parsed_args.router, ignore_missing=False |
679 | ++ ) |
680 | ++ |
681 | ++ # Get the common attributes. |
682 | ++ attrs = _get_external_gateway_attrs( |
683 | ++ self.app.client_manager, parsed_args |
684 | ++ ) |
685 | ++ |
686 | ++ if attrs: |
687 | ++ external_gateways = attrs.pop('external_gateways') |
688 | ++ router_obj = client.add_external_gateways( |
689 | ++ router_obj, |
690 | ++ body={'router': {'external_gateways': external_gateways}}, |
691 | ++ ) |
692 | ++ |
693 | ++ display_columns, columns = _get_columns(router_obj) |
694 | ++ data = utils.get_item_properties( |
695 | ++ router_obj, columns, formatters=_formatters |
696 | ++ ) |
697 | ++ return (display_columns, data) |
698 | ++ |
699 | ++ |
700 | ++class RemoveGatewayFromRouter(command.ShowOne): |
701 | ++ _description = _("Remove router gateway") |
702 | ++ |
703 | ++ def get_parser(self, prog_name): |
704 | ++ parser = super().get_parser(prog_name) |
705 | ++ parser.add_argument( |
706 | ++ 'router', |
707 | ++ metavar="<router>", |
708 | ++ help=_("Router to modify (name or ID)."), |
709 | ++ ) |
710 | ++ parser.add_argument( |
711 | ++ metavar="<network>", |
712 | ++ help=_( |
713 | ++ "External Network to remove a router gateway from (name or " |
714 | ++ "ID)." |
715 | ++ ), |
716 | ++ dest='external_gateways', |
717 | ++ # The argument is stored in a list in order to reuse the |
718 | ++ # common attribute parsing code. |
719 | ++ nargs=1, |
720 | ++ ) |
721 | ++ parser.add_argument( |
722 | ++ '--fixed-ip', |
723 | ++ metavar='subnet=<subnet>,ip-address=<ip-address>', |
724 | ++ action=parseractions.MultiKeyValueAction, |
725 | ++ optional_keys=['subnet', 'ip-address'], |
726 | ++ dest='fixed_ips', |
727 | ++ help=_( |
728 | ++ "IP and/or subnet (name or ID) on the external gateway " |
729 | ++ "which is used to identify a particular gateway if multiple " |
730 | ++ "are attached to the same network: subnet=<subnet>," |
731 | ++ "ip-address=<ip-address>." |
732 | ++ ), |
733 | ++ ) |
734 | ++ return parser |
735 | ++ |
736 | ++ def take_action(self, parsed_args): |
737 | ++ client = self.app.client_manager.network |
738 | ++ if not is_multiple_gateways_supported(client): |
739 | ++ msg = _( |
740 | ++ 'The external-gateway-multihoming extension is not enabled at ' |
741 | ++ 'the Neutron side.' |
742 | ++ ) |
743 | ++ raise exceptions.CommandError(msg) |
744 | ++ |
745 | ++ router_obj = client.find_router( |
746 | ++ parsed_args.router, ignore_missing=False |
747 | ++ ) |
748 | ++ |
749 | ++ # Get the common attributes. |
750 | ++ attrs = _get_external_gateway_attrs( |
751 | ++ self.app.client_manager, parsed_args |
752 | ++ ) |
753 | ++ if attrs: |
754 | ++ external_gateways = attrs.pop('external_gateways') |
755 | ++ router_obj = client.remove_external_gateways( |
756 | ++ router_obj, |
757 | ++ body={'router': {'external_gateways': external_gateways}}, |
758 | ++ ) |
759 | ++ |
760 | ++ display_columns, columns = _get_columns(router_obj) |
761 | ++ data = utils.get_item_properties( |
762 | ++ router_obj, columns, formatters=_formatters |
763 | ++ ) |
764 | ++ return (display_columns, data) |
765 | +diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py |
766 | +index fb12e3f0..33956b3e 100644 |
767 | +--- a/openstackclient/tests/unit/network/v2/test_router.py |
768 | ++++ b/openstackclient/tests/unit/network/v2/test_router.py |
769 | +@@ -75,7 +75,7 @@ class TestAddPortToRouter(TestRouter): |
770 | + self._router, |
771 | + **{ |
772 | + 'port_id': self._router.port, |
773 | +- } |
774 | ++ }, |
775 | + ) |
776 | + self.assertIsNone(result) |
777 | + |
778 | +@@ -130,6 +130,7 @@ class TestAddSubnetToRouter(TestRouter): |
779 | + class TestCreateRouter(TestRouter): |
780 | + # The new router created. |
781 | + new_router = network_fakes.FakeRouter.create_one_router() |
782 | ++ _extensions = {'fake': network_fakes.create_one_extension()} |
783 | + |
784 | + columns = ( |
785 | + 'admin_state_up', |
786 | +@@ -169,7 +170,9 @@ class TestCreateRouter(TestRouter): |
787 | + return_value=self.new_router |
788 | + ) |
789 | + self.network_client.set_tags = mock.Mock(return_value=None) |
790 | +- |
791 | ++ self.network_client.find_extension = mock.Mock( |
792 | ++ side_effect=lambda name: self._extensions.get(name) |
793 | ++ ) |
794 | + # Get the command object to test |
795 | + self.cmd = router.CreateRouter(self.app, self.namespace) |
796 | + |
797 | +@@ -228,7 +231,7 @@ class TestCreateRouter(TestRouter): |
798 | + ('enable', True), |
799 | + ('distributed', False), |
800 | + ('ha', False), |
801 | +- ('external_gateway', _network.name), |
802 | ++ ('external_gateways', [_network.name]), |
803 | + ('enable_snat', True), |
804 | + ('fixed_ips', [{'ip-address': '2001:db8::1'}]), |
805 | + ] |
806 | +@@ -1100,10 +1103,13 @@ class TestSetRouter(TestRouter): |
807 | + # The router to set. |
808 | + _default_route = {'destination': '10.20.20.0/24', 'nexthop': '10.20.30.1'} |
809 | + _network = network_fakes.create_one_network() |
810 | +- _subnet = network_fakes.FakeSubnet.create_one_subnet() |
811 | ++ _subnet = network_fakes.FakeSubnet.create_one_subnet( |
812 | ++ attrs={'network_id': _network.id} |
813 | ++ ) |
814 | + _router = network_fakes.FakeRouter.create_one_router( |
815 | + attrs={'routes': [_default_route], 'tags': ['green', 'red']} |
816 | + ) |
817 | ++ _extensions = {'fake': network_fakes.create_one_extension()} |
818 | + |
819 | + def setUp(self): |
820 | + super(TestSetRouter, self).setUp() |
821 | +@@ -1114,7 +1120,9 @@ class TestSetRouter(TestRouter): |
822 | + return_value=self._network |
823 | + ) |
824 | + self.network_client.find_subnet = mock.Mock(return_value=self._subnet) |
825 | +- |
826 | ++ self.network_client.find_extension = mock.Mock( |
827 | ++ side_effect=lambda name: self._extensions.get(name) |
828 | ++ ) |
829 | + # Get the command object to test |
830 | + self.cmd = router.SetRouter(self.app, self.namespace) |
831 | + |
832 | +@@ -1312,7 +1320,7 @@ class TestSetRouter(TestRouter): |
833 | + self._router.id, |
834 | + ] |
835 | + verifylist = [ |
836 | +- ('external_gateway', self._network.id), |
837 | ++ ('external_gateways', [self._network.id]), |
838 | + ('router', self._router.id), |
839 | + ] |
840 | + parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
841 | +@@ -1320,7 +1328,7 @@ class TestSetRouter(TestRouter): |
842 | + result = self.cmd.take_action(parsed_args) |
843 | + self.network_client.update_router.assert_called_with( |
844 | + self._router, |
845 | +- **{'external_gateway_info': {'network_id': self._network.id}} |
846 | ++ **{'external_gateway_info': {'network_id': self._network.id}}, |
847 | + ) |
848 | + self.assertIsNone(result) |
849 | + |
850 | +@@ -1335,7 +1343,7 @@ class TestSetRouter(TestRouter): |
851 | + ] |
852 | + verifylist = [ |
853 | + ('router', self._router.id), |
854 | +- ('external_gateway', self._network.id), |
855 | ++ ('external_gateways', [self._network.id]), |
856 | + ('fixed_ips', [{'subnet': "'abc'"}]), |
857 | + ('enable_snat', True), |
858 | + ] |
859 | +@@ -1354,7 +1362,7 @@ class TestSetRouter(TestRouter): |
860 | + ], |
861 | + 'enable_snat': True, |
862 | + } |
863 | +- } |
864 | ++ }, |
865 | + ) |
866 | + self.assertIsNone(result) |
867 | + |
868 | +@@ -1369,7 +1377,7 @@ class TestSetRouter(TestRouter): |
869 | + ] |
870 | + verifylist = [ |
871 | + ('router', self._router.id), |
872 | +- ('external_gateway', self._network.id), |
873 | ++ ('external_gateways', [self._network.id]), |
874 | + ('fixed_ips', [{'ip-address': "10.0.1.1"}]), |
875 | + ('enable_snat', True), |
876 | + ] |
877 | +@@ -1388,7 +1396,7 @@ class TestSetRouter(TestRouter): |
878 | + ], |
879 | + 'enable_snat': True, |
880 | + } |
881 | +- } |
882 | ++ }, |
883 | + ) |
884 | + self.assertIsNone(result) |
885 | + |
886 | +@@ -1403,7 +1411,7 @@ class TestSetRouter(TestRouter): |
887 | + ] |
888 | + verifylist = [ |
889 | + ('router', self._router.id), |
890 | +- ('external_gateway', self._network.id), |
891 | ++ ('external_gateways', [self._network.id]), |
892 | + ('fixed_ips', [{'subnet': "'abc'", 'ip-address': "10.0.1.1"}]), |
893 | + ('enable_snat', True), |
894 | + ] |
895 | +@@ -1423,7 +1431,7 @@ class TestSetRouter(TestRouter): |
896 | + ], |
897 | + 'enable_snat': True, |
898 | + } |
899 | +- } |
900 | ++ }, |
901 | + ) |
902 | + self.assertIsNone(result) |
903 | + |
904 | +@@ -1468,7 +1476,7 @@ class TestSetRouter(TestRouter): |
905 | + ] |
906 | + verifylist = [ |
907 | + ('router', self._router.id), |
908 | +- ('external_gateway', self._network.id), |
909 | ++ ('external_gateways', [self._network.id]), |
910 | + ('qos_policy', qos_policy.id), |
911 | + ] |
912 | + parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
913 | +@@ -1481,7 +1489,7 @@ class TestSetRouter(TestRouter): |
914 | + 'network_id': self._network.id, |
915 | + 'qos_policy_id': qos_policy.id, |
916 | + } |
917 | +- } |
918 | ++ }, |
919 | + ) |
920 | + self.assertIsNone(result) |
921 | + |
922 | +@@ -1494,7 +1502,7 @@ class TestSetRouter(TestRouter): |
923 | + ] |
924 | + verifylist = [ |
925 | + ('router', self._router.id), |
926 | +- ('external_gateway', self._network.id), |
927 | ++ ('external_gateways', [self._network.id]), |
928 | + ('no_qos_policy', True), |
929 | + ] |
930 | + parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
931 | +@@ -1507,7 +1515,7 @@ class TestSetRouter(TestRouter): |
932 | + 'network_id': self._network.id, |
933 | + 'qos_policy_id': None, |
934 | + } |
935 | +- } |
936 | ++ }, |
937 | + ) |
938 | + self.assertIsNone(result) |
939 | + |
940 | +@@ -1526,7 +1534,7 @@ class TestSetRouter(TestRouter): |
941 | + ] |
942 | + verifylist = [ |
943 | + ('router', self._router.id), |
944 | +- ('external_gateway', self._network.id), |
945 | ++ ('external_gateways', [self._network.id]), |
946 | + ('qos_policy', qos_policy.id), |
947 | + ('no_qos_policy', True), |
948 | + ] |
949 | +@@ -1747,6 +1755,13 @@ class TestUnsetRouter(TestRouter): |
950 | + ) |
951 | + self.network_client.update_router = mock.Mock(return_value=None) |
952 | + self.network_client.set_tags = mock.Mock(return_value=None) |
953 | ++ self._extensions = {'fake': network_fakes.create_one_extension()} |
954 | ++ self.network_client.find_extension = mock.Mock( |
955 | ++ side_effect=lambda name: self._extensions.get(name) |
956 | ++ ) |
957 | ++ self.network_client.remove_external_gateways = mock.Mock( |
958 | ++ return_value=None |
959 | ++ ) |
960 | + # Get the command object to test |
961 | + self.cmd = router.UnsetRouter(self.app, self.namespace) |
962 | + |
963 | +@@ -1799,7 +1814,7 @@ class TestUnsetRouter(TestRouter): |
964 | + '--external-gateway', |
965 | + self._testrouter.name, |
966 | + ] |
967 | +- verifylist = [('external_gateway', True)] |
968 | ++ verifylist = [('external_gateways', True)] |
969 | + parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
970 | + result = self.cmd.take_action(parsed_args) |
971 | + attrs = {'external_gateway_info': {}} |
972 | +@@ -1808,6 +1823,33 @@ class TestUnsetRouter(TestRouter): |
973 | + ) |
974 | + self.assertIsNone(result) |
975 | + |
976 | ++ def test_unset_router_external_gateway_multiple_supported(self): |
977 | ++ # Add the relevant extension in order to test the alternate behavior. |
978 | ++ self._extensions = { |
979 | ++ 'external-gateway-multihoming': network_fakes.create_one_extension( |
980 | ++ attrs={'name': 'external-gateway-multihoming'} |
981 | ++ ) |
982 | ++ } |
983 | ++ arglist = [ |
984 | ++ '--external-gateway', |
985 | ++ self._testrouter.name, |
986 | ++ ] |
987 | ++ verifylist = [('external_gateways', True)] |
988 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
989 | ++ result = self.cmd.take_action(parsed_args) |
990 | ++ # The removal of all gateways should be requested using the multiple |
991 | ++ # gateways API. |
992 | ++ self.network_client.remove_external_gateways.assert_called_once_with( |
993 | ++ self._testrouter, body={'router': {'external_gateways': {}}} |
994 | ++ ) |
995 | ++ # The compatibility API will also be called in order to potentially |
996 | ++ # unset other parameters along with external_gateway_info which |
997 | ++ # should already be empty at that point anyway. |
998 | ++ self.network_client.update_router.assert_called_once_with( |
999 | ++ self._testrouter, **{'external_gateway_info': {}} |
1000 | ++ ) |
1001 | ++ self.assertIsNone(result) |
1002 | ++ |
1003 | + def _test_unset_tags(self, with_tags=True): |
1004 | + if with_tags: |
1005 | + arglist = ['--tag', 'red', '--tag', 'blue'] |
1006 | +@@ -1895,3 +1937,539 @@ class TestUnsetRouter(TestRouter): |
1007 | + self.assertRaises( |
1008 | + exceptions.CommandError, self.cmd.take_action, parsed_args |
1009 | + ) |
1010 | ++ |
1011 | ++ |
1012 | ++class TestGatewayOps(TestRouter): |
1013 | ++ def setUp(self): |
1014 | ++ super().setUp() |
1015 | ++ self._networks = [] |
1016 | ++ self._network = network_fakes.create_one_network() |
1017 | ++ self._networks.append(self._network) |
1018 | ++ |
1019 | ++ self._router = network_fakes.FakeRouter.create_one_router( |
1020 | ++ { |
1021 | ++ 'external_gateway_info': { |
1022 | ++ 'network_id': self._network.id, |
1023 | ++ }, |
1024 | ++ } |
1025 | ++ ) |
1026 | ++ self._subnet = network_fakes.FakeSubnet.create_one_subnet( |
1027 | ++ attrs={'network_id': self._network.id} |
1028 | ++ ) |
1029 | ++ self._extensions = { |
1030 | ++ 'external-gateway-multihoming': network_fakes.create_one_extension( |
1031 | ++ attrs={'name': 'external-gateway-multihoming'} |
1032 | ++ ) |
1033 | ++ } |
1034 | ++ self.network_client.find_extension = mock.Mock( |
1035 | ++ side_effect=lambda name: self._extensions.get(name) |
1036 | ++ ) |
1037 | ++ self.network_client.find_router = mock.Mock(return_value=self._router) |
1038 | ++ |
1039 | ++ def _find_network(name_or_id, ignore_missing): |
1040 | ++ for network in self._networks: |
1041 | ++ if name_or_id in (network.id, network.name): |
1042 | ++ return network |
1043 | ++ if ignore_missing: |
1044 | ++ return None |
1045 | ++ raise Exception('Test resource not found') |
1046 | ++ |
1047 | ++ self.network_client.find_network = mock.Mock(side_effect=_find_network) |
1048 | ++ |
1049 | ++ self.network_client.find_subnet = mock.Mock(return_value=self._subnet) |
1050 | ++ self.network_client.add_external_gateways = mock.Mock( |
1051 | ++ return_value=None |
1052 | ++ ) |
1053 | ++ self.network_client.remove_external_gateways = mock.Mock( |
1054 | ++ return_value=None |
1055 | ++ ) |
1056 | ++ |
1057 | ++ |
1058 | ++class TestCreateMultipleGateways(TestGatewayOps): |
1059 | ++ _columns = ( |
1060 | ++ 'admin_state_up', |
1061 | ++ 'availability_zone_hints', |
1062 | ++ 'availability_zones', |
1063 | ++ 'description', |
1064 | ++ 'distributed', |
1065 | ++ 'external_gateway_info', |
1066 | ++ 'ha', |
1067 | ++ 'id', |
1068 | ++ 'name', |
1069 | ++ 'project_id', |
1070 | ++ 'routes', |
1071 | ++ 'status', |
1072 | ++ 'tags', |
1073 | ++ ) |
1074 | ++ |
1075 | ++ def setUp(self): |
1076 | ++ super().setUp() |
1077 | ++ self._second_network = network_fakes.create_one_network() |
1078 | ++ self._networks.append(self._second_network) |
1079 | ++ |
1080 | ++ self.network_client.create_router = mock.Mock( |
1081 | ++ return_value=self._router |
1082 | ++ ) |
1083 | ++ self.network_client.update_router = mock.Mock(return_value=None) |
1084 | ++ self.network_client.update_external_gateways = mock.Mock( |
1085 | ++ return_value=None |
1086 | ++ ) |
1087 | ++ |
1088 | ++ self._data = ( |
1089 | ++ router.AdminStateColumn(self._router.admin_state_up), |
1090 | ++ format_columns.ListColumn(self._router.availability_zone_hints), |
1091 | ++ format_columns.ListColumn(self._router.availability_zones), |
1092 | ++ self._router.description, |
1093 | ++ self._router.distributed, |
1094 | ++ router.RouterInfoColumn(self._router.external_gateway_info), |
1095 | ++ self._router.ha, |
1096 | ++ self._router.id, |
1097 | ++ self._router.name, |
1098 | ++ self._router.project_id, |
1099 | ++ router.RoutesColumn(self._router.routes), |
1100 | ++ self._router.status, |
1101 | ++ format_columns.ListColumn(self._router.tags), |
1102 | ++ ) |
1103 | ++ self.cmd = router.CreateRouter(self.app, self.namespace) |
1104 | ++ |
1105 | ++ def test_create_one_gateway(self): |
1106 | ++ arglist = [ |
1107 | ++ "--external-gateway", |
1108 | ++ self._network.id, |
1109 | ++ self._router.name, |
1110 | ++ ] |
1111 | ++ verifylist = [ |
1112 | ++ ('name', self._router.name), |
1113 | ++ ('external_gateways', [self._network.id]), |
1114 | ++ ] |
1115 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1116 | ++ |
1117 | ++ columns, data = self.cmd.take_action(parsed_args) |
1118 | ++ self.network_client.update_external_gateways.assert_called_with( |
1119 | ++ self._router, |
1120 | ++ body={ |
1121 | ++ 'router': { |
1122 | ++ 'external_gateways': [ |
1123 | ++ { |
1124 | ++ 'network_id': self._network.id, |
1125 | ++ } |
1126 | ++ ] |
1127 | ++ } |
1128 | ++ }, |
1129 | ++ ) |
1130 | ++ self.assertEqual(self._columns, columns) |
1131 | ++ self.assertCountEqual(self._data, data) |
1132 | ++ |
1133 | ++ def test_create_multiple_gateways(self): |
1134 | ++ arglist = [ |
1135 | ++ self._router.name, |
1136 | ++ "--external-gateway", |
1137 | ++ self._network.id, |
1138 | ++ "--external-gateway", |
1139 | ++ self._network.id, |
1140 | ++ "--external-gateway", |
1141 | ++ self._second_network.id, |
1142 | ++ '--fixed-ip', |
1143 | ++ 'subnet={},ip-address=10.0.1.1'.format(self._subnet.id), |
1144 | ++ '--fixed-ip', |
1145 | ++ 'subnet={},ip-address=10.0.1.2'.format(self._subnet.id), |
1146 | ++ ] |
1147 | ++ verifylist = [ |
1148 | ++ ('name', self._router.name), |
1149 | ++ ( |
1150 | ++ 'external_gateways', |
1151 | ++ [self._network.id, self._network.id, self._second_network.id], |
1152 | ++ ), |
1153 | ++ ] |
1154 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1155 | ++ columns, data = self.cmd.take_action(parsed_args) |
1156 | ++ |
1157 | ++ # The router will not have a gateway after the create call, but it |
1158 | ++ # will be added after the update call. |
1159 | ++ self.network_client.create_router.assert_called_once_with( |
1160 | ++ **{ |
1161 | ++ 'admin_state_up': True, |
1162 | ++ 'name': self._router.name, |
1163 | ++ } |
1164 | ++ ) |
1165 | ++ self.network_client.update_external_gateways.assert_called_with( |
1166 | ++ self._router, |
1167 | ++ body={ |
1168 | ++ 'router': { |
1169 | ++ 'external_gateways': [ |
1170 | ++ { |
1171 | ++ 'network_id': self._network.id, |
1172 | ++ 'external_fixed_ips': [ |
1173 | ++ { |
1174 | ++ 'subnet_id': self._subnet.id, |
1175 | ++ 'ip_address': '10.0.1.1', |
1176 | ++ } |
1177 | ++ ], |
1178 | ++ }, |
1179 | ++ { |
1180 | ++ 'network_id': self._network.id, |
1181 | ++ 'external_fixed_ips': [ |
1182 | ++ { |
1183 | ++ 'subnet_id': self._subnet.id, |
1184 | ++ 'ip_address': '10.0.1.2', |
1185 | ++ } |
1186 | ++ ], |
1187 | ++ }, |
1188 | ++ { |
1189 | ++ 'network_id': self._second_network.id, |
1190 | ++ }, |
1191 | ++ ] |
1192 | ++ } |
1193 | ++ }, |
1194 | ++ ) |
1195 | ++ self.assertEqual(self._columns, columns) |
1196 | ++ self.assertCountEqual(self._data, data) |
1197 | ++ |
1198 | ++ |
1199 | ++class TestUpdateMultipleGateways(TestGatewayOps): |
1200 | ++ def setUp(self): |
1201 | ++ super().setUp() |
1202 | ++ self._second_network = network_fakes.create_one_network() |
1203 | ++ self._networks.append(self._second_network) |
1204 | ++ |
1205 | ++ self.network_client.update_router = mock.Mock(return_value=None) |
1206 | ++ self.network_client.update_external_gateways = mock.Mock( |
1207 | ++ return_value=None |
1208 | ++ ) |
1209 | ++ self.cmd = router.SetRouter(self.app, self.namespace) |
1210 | ++ |
1211 | ++ def test_update_one_gateway(self): |
1212 | ++ arglist = [ |
1213 | ++ "--external-gateway", |
1214 | ++ self._network.id, |
1215 | ++ "--no-qos-policy", |
1216 | ++ self._router.name, |
1217 | ++ ] |
1218 | ++ verifylist = [ |
1219 | ++ ('router', self._router.name), |
1220 | ++ ('external_gateways', [self._network.id]), |
1221 | ++ ('no_qos_policy', True), |
1222 | ++ ] |
1223 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1224 | ++ result = self.cmd.take_action(parsed_args) |
1225 | ++ self.network_client.update_external_gateways.assert_called_with( |
1226 | ++ self._router, |
1227 | ++ body={ |
1228 | ++ 'router': { |
1229 | ++ 'external_gateways': [ |
1230 | ++ {'network_id': self._network.id, 'qos_policy_id': None} |
1231 | ++ ] |
1232 | ++ } |
1233 | ++ }, |
1234 | ++ ) |
1235 | ++ self.assertIsNone(result) |
1236 | ++ |
1237 | ++ def test_update_multiple_gateways(self): |
1238 | ++ arglist = [ |
1239 | ++ self._router.name, |
1240 | ++ "--external-gateway", |
1241 | ++ self._network.id, |
1242 | ++ "--external-gateway", |
1243 | ++ self._network.id, |
1244 | ++ "--external-gateway", |
1245 | ++ self._second_network.id, |
1246 | ++ '--fixed-ip', |
1247 | ++ 'subnet={},ip-address=10.0.1.1'.format(self._subnet.id), |
1248 | ++ '--fixed-ip', |
1249 | ++ 'subnet={},ip-address=10.0.1.2'.format(self._subnet.id), |
1250 | ++ "--no-qos-policy", |
1251 | ++ ] |
1252 | ++ verifylist = [ |
1253 | ++ ('router', self._router.name), |
1254 | ++ ( |
1255 | ++ 'external_gateways', |
1256 | ++ [self._network.id, self._network.id, self._second_network.id], |
1257 | ++ ), |
1258 | ++ ('no_qos_policy', True), |
1259 | ++ ] |
1260 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1261 | ++ result = self.cmd.take_action(parsed_args) |
1262 | ++ self.network_client.update_external_gateways.assert_called_with( |
1263 | ++ self._router, |
1264 | ++ body={ |
1265 | ++ 'router': { |
1266 | ++ 'external_gateways': [ |
1267 | ++ { |
1268 | ++ 'network_id': self._network.id, |
1269 | ++ 'external_fixed_ips': [ |
1270 | ++ { |
1271 | ++ 'subnet_id': self._subnet.id, |
1272 | ++ 'ip_address': '10.0.1.1', |
1273 | ++ } |
1274 | ++ ], |
1275 | ++ 'qos_policy_id': None, |
1276 | ++ }, |
1277 | ++ { |
1278 | ++ 'network_id': self._network.id, |
1279 | ++ 'external_fixed_ips': [ |
1280 | ++ { |
1281 | ++ 'subnet_id': self._subnet.id, |
1282 | ++ 'ip_address': '10.0.1.2', |
1283 | ++ } |
1284 | ++ ], |
1285 | ++ 'qos_policy_id': None, |
1286 | ++ }, |
1287 | ++ { |
1288 | ++ 'network_id': self._second_network.id, |
1289 | ++ 'qos_policy_id': None, |
1290 | ++ }, |
1291 | ++ ] |
1292 | ++ } |
1293 | ++ }, |
1294 | ++ ) |
1295 | ++ self.assertIsNone(result) |
1296 | ++ |
1297 | ++ |
1298 | ++class TestAddGatewayRouter(TestGatewayOps): |
1299 | ++ def setUp(self): |
1300 | ++ super().setUp() |
1301 | ++ # Get the command object to test |
1302 | ++ self.cmd = router.AddGatewayToRouter(self.app, self.namespace) |
1303 | ++ |
1304 | ++ self.network_client.add_external_gateways.return_value = self._router |
1305 | ++ |
1306 | ++ def test_add_gateway_network_only(self): |
1307 | ++ arglist = [ |
1308 | ++ self._router.name, |
1309 | ++ self._network.id, |
1310 | ++ ] |
1311 | ++ verifylist = [ |
1312 | ++ ('router', self._router.name), |
1313 | ++ ('external_gateways', [self._network.id]), |
1314 | ++ ] |
1315 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1316 | ++ result = self.cmd.take_action(parsed_args) |
1317 | ++ self.network_client.add_external_gateways.assert_called_with( |
1318 | ++ self._router, |
1319 | ++ body={ |
1320 | ++ 'router': { |
1321 | ++ 'external_gateways': [{'network_id': self._network.id}] |
1322 | ++ } |
1323 | ++ }, |
1324 | ++ ) |
1325 | ++ self.assertEqual(result[1][result[0].index('id')], self._router.id) |
1326 | ++ |
1327 | ++ def test_add_gateway_network_fixed_ip(self): |
1328 | ++ arglist = [ |
1329 | ++ self._router.name, |
1330 | ++ self._network.id, |
1331 | ++ '--fixed-ip', |
1332 | ++ 'subnet={},ip-address=10.0.1.1'.format(self._subnet.id), |
1333 | ++ ] |
1334 | ++ verifylist = [ |
1335 | ++ ('router', self._router.name), |
1336 | ++ ('external_gateways', [self._network.id]), |
1337 | ++ ] |
1338 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1339 | ++ result = self.cmd.take_action(parsed_args) |
1340 | ++ self.network_client.add_external_gateways.assert_called_with( |
1341 | ++ self._router, |
1342 | ++ body={ |
1343 | ++ 'router': { |
1344 | ++ 'external_gateways': [ |
1345 | ++ { |
1346 | ++ 'network_id': self._network.id, |
1347 | ++ 'external_fixed_ips': [ |
1348 | ++ { |
1349 | ++ 'subnet_id': self._subnet.id, |
1350 | ++ 'ip_address': '10.0.1.1', |
1351 | ++ } |
1352 | ++ ], |
1353 | ++ } |
1354 | ++ ] |
1355 | ++ } |
1356 | ++ }, |
1357 | ++ ) |
1358 | ++ self.assertEqual(result[1][result[0].index('id')], self._router.id) |
1359 | ++ |
1360 | ++ def test_add_gateway_network_multiple_fixed_ips(self): |
1361 | ++ arglist = [ |
1362 | ++ self._router.name, |
1363 | ++ self._network.id, |
1364 | ++ '--fixed-ip', |
1365 | ++ 'subnet={},ip-address=10.0.1.1'.format(self._subnet.id), |
1366 | ++ '--fixed-ip', |
1367 | ++ 'subnet={},ip-address=10.0.1.2'.format(self._subnet.id), |
1368 | ++ ] |
1369 | ++ verifylist = [ |
1370 | ++ ('router', self._router.name), |
1371 | ++ ('external_gateways', [self._network.id]), |
1372 | ++ ( |
1373 | ++ 'fixed_ips', |
1374 | ++ [ |
1375 | ++ {'ip-address': '10.0.1.1', 'subnet': self._subnet.id}, |
1376 | ++ {'ip-address': '10.0.1.2', 'subnet': self._subnet.id}, |
1377 | ++ ], |
1378 | ++ ), |
1379 | ++ ] |
1380 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1381 | ++ result = self.cmd.take_action(parsed_args) |
1382 | ++ self.network_client.add_external_gateways.assert_called_with( |
1383 | ++ self._router, |
1384 | ++ body={ |
1385 | ++ 'router': { |
1386 | ++ 'external_gateways': [ |
1387 | ++ { |
1388 | ++ 'network_id': self._network.id, |
1389 | ++ 'external_fixed_ips': [ |
1390 | ++ { |
1391 | ++ 'subnet_id': self._subnet.id, |
1392 | ++ 'ip_address': '10.0.1.1', |
1393 | ++ }, |
1394 | ++ { |
1395 | ++ 'subnet_id': self._subnet.id, |
1396 | ++ 'ip_address': '10.0.1.2', |
1397 | ++ }, |
1398 | ++ ], |
1399 | ++ } |
1400 | ++ ] |
1401 | ++ } |
1402 | ++ }, |
1403 | ++ ) |
1404 | ++ self.assertEqual(result[1][result[0].index('id')], self._router.id) |
1405 | ++ |
1406 | ++ def test_add_gateway_network_only_no_extension(self): |
1407 | ++ self._extensions = {} |
1408 | ++ arglist = [ |
1409 | ++ self._router.name, |
1410 | ++ self._network.id, |
1411 | ++ ] |
1412 | ++ verifylist = [ |
1413 | ++ ('router', self._router.name), |
1414 | ++ ('external_gateways', [self._network.id]), |
1415 | ++ ] |
1416 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1417 | ++ self.assertRaises( |
1418 | ++ exceptions.CommandError, self.cmd.take_action, parsed_args |
1419 | ++ ) |
1420 | ++ |
1421 | ++ |
1422 | ++class TestRemoveGatewayRouter(TestGatewayOps): |
1423 | ++ def setUp(self): |
1424 | ++ super().setUp() |
1425 | ++ # Get the command object to test |
1426 | ++ self.cmd = router.RemoveGatewayFromRouter(self.app, self.namespace) |
1427 | ++ |
1428 | ++ self.network_client.remove_external_gateways.return_value = ( |
1429 | ++ self._router |
1430 | ++ ) |
1431 | ++ |
1432 | ++ def test_remove_gateway_network_only(self): |
1433 | ++ arglist = [ |
1434 | ++ self._router.name, |
1435 | ++ self._network.id, |
1436 | ++ ] |
1437 | ++ verifylist = [ |
1438 | ++ ('router', self._router.name), |
1439 | ++ ('external_gateways', [self._network.id]), |
1440 | ++ ] |
1441 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1442 | ++ result = self.cmd.take_action(parsed_args) |
1443 | ++ self.network_client.remove_external_gateways.assert_called_with( |
1444 | ++ self._router, |
1445 | ++ body={ |
1446 | ++ 'router': { |
1447 | ++ 'external_gateways': [{'network_id': self._network.id}] |
1448 | ++ } |
1449 | ++ }, |
1450 | ++ ) |
1451 | ++ self.assertEqual(result[1][result[0].index('id')], self._router.id) |
1452 | ++ |
1453 | ++ def test_remove_gateway_network_fixed_ip(self): |
1454 | ++ arglist = [ |
1455 | ++ self._router.name, |
1456 | ++ self._network.id, |
1457 | ++ '--fixed-ip', |
1458 | ++ 'subnet={},ip-address=10.0.1.1'.format(self._subnet.id), |
1459 | ++ ] |
1460 | ++ verifylist = [ |
1461 | ++ ('router', self._router.name), |
1462 | ++ ('external_gateways', [self._network.id]), |
1463 | ++ ] |
1464 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1465 | ++ result = self.cmd.take_action(parsed_args) |
1466 | ++ self.network_client.remove_external_gateways.assert_called_with( |
1467 | ++ self._router, |
1468 | ++ body={ |
1469 | ++ 'router': { |
1470 | ++ 'external_gateways': [ |
1471 | ++ { |
1472 | ++ 'network_id': self._network.id, |
1473 | ++ 'external_fixed_ips': [ |
1474 | ++ { |
1475 | ++ 'subnet_id': self._subnet.id, |
1476 | ++ 'ip_address': '10.0.1.1', |
1477 | ++ } |
1478 | ++ ], |
1479 | ++ } |
1480 | ++ ] |
1481 | ++ } |
1482 | ++ }, |
1483 | ++ ) |
1484 | ++ self.assertEqual(result[1][result[0].index('id')], self._router.id) |
1485 | ++ |
1486 | ++ def test_remove_gateway_network_multiple_fixed_ips(self): |
1487 | ++ arglist = [ |
1488 | ++ self._router.name, |
1489 | ++ self._network.id, |
1490 | ++ '--fixed-ip', |
1491 | ++ 'subnet={},ip-address=10.0.1.1'.format(self._subnet.id), |
1492 | ++ '--fixed-ip', |
1493 | ++ 'subnet={},ip-address=10.0.1.2'.format(self._subnet.id), |
1494 | ++ ] |
1495 | ++ verifylist = [ |
1496 | ++ ('router', self._router.name), |
1497 | ++ ('external_gateways', [self._network.id]), |
1498 | ++ ( |
1499 | ++ 'fixed_ips', |
1500 | ++ [ |
1501 | ++ {'ip-address': '10.0.1.1', 'subnet': self._subnet.id}, |
1502 | ++ {'ip-address': '10.0.1.2', 'subnet': self._subnet.id}, |
1503 | ++ ], |
1504 | ++ ), |
1505 | ++ ] |
1506 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1507 | ++ result = self.cmd.take_action(parsed_args) |
1508 | ++ self.network_client.remove_external_gateways.assert_called_with( |
1509 | ++ self._router, |
1510 | ++ body={ |
1511 | ++ 'router': { |
1512 | ++ 'external_gateways': [ |
1513 | ++ { |
1514 | ++ 'network_id': self._network.id, |
1515 | ++ 'external_fixed_ips': [ |
1516 | ++ { |
1517 | ++ 'subnet_id': self._subnet.id, |
1518 | ++ 'ip_address': '10.0.1.1', |
1519 | ++ }, |
1520 | ++ { |
1521 | ++ 'subnet_id': self._subnet.id, |
1522 | ++ 'ip_address': '10.0.1.2', |
1523 | ++ }, |
1524 | ++ ], |
1525 | ++ } |
1526 | ++ ] |
1527 | ++ } |
1528 | ++ }, |
1529 | ++ ) |
1530 | ++ self.assertEqual(result[1][result[0].index('id')], self._router.id) |
1531 | ++ |
1532 | ++ def test_remove_gateway_network_only_no_extension(self): |
1533 | ++ self._extensions = {} |
1534 | ++ arglist = [ |
1535 | ++ self._router.name, |
1536 | ++ self._network.id, |
1537 | ++ ] |
1538 | ++ verifylist = [ |
1539 | ++ ('router', self._router.name), |
1540 | ++ ('external_gateways', [self._network.id]), |
1541 | ++ ] |
1542 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1543 | ++ self.assertRaises( |
1544 | ++ exceptions.CommandError, self.cmd.take_action, parsed_args |
1545 | ++ ) |
1546 | +diff -ru a/setup.cfg b/setup.cfg |
1547 | +--- a/setup.cfg 2024-03-18 14:06:34.385786800 +0000 |
1548 | ++++ b/setup.cfg 2024-04-09 06:36:40.483069266 +0000 |
1549 | +@@ -551,12 +551,14 @@ |
1550 | + port_show = openstackclient.network.v2.port:ShowPort |
1551 | + port_unset = openstackclient.network.v2.port:UnsetPort |
1552 | + |
1553 | ++ router_add_gateway = openstackclient.network.v2.router:AddGatewayToRouter |
1554 | + router_add_port = openstackclient.network.v2.router:AddPortToRouter |
1555 | + router_add_route = openstackclient.network.v2.router:AddExtraRoutesToRouter |
1556 | + router_add_subnet = openstackclient.network.v2.router:AddSubnetToRouter |
1557 | + router_create = openstackclient.network.v2.router:CreateRouter |
1558 | + router_delete = openstackclient.network.v2.router:DeleteRouter |
1559 | + router_list = openstackclient.network.v2.router:ListRouter |
1560 | ++ router_remove_gateway = openstackclient.network.v2.router:RemoveGatewayFromRouter |
1561 | + router_remove_port = openstackclient.network.v2.router:RemovePortFromRouter |
1562 | + router_remove_route = openstackclient.network.v2.router:RemoveExtraRoutesFromRouter |
1563 | + router_remove_subnet = openstackclient.network.v2.router:RemoveSubnetFromRouter |
1564 | +-- |
1565 | +2.43.0 |
1566 | + |
1567 | diff --git a/debian/patches/lp-2002687-4-Add-router-default-route-BFD-ECMP-options.patch b/debian/patches/lp-2002687-4-Add-router-default-route-BFD-ECMP-options.patch |
1568 | new file mode 100644 |
1569 | index 0000000..aceb773 |
1570 | --- /dev/null |
1571 | +++ b/debian/patches/lp-2002687-4-Add-router-default-route-BFD-ECMP-options.patch |
1572 | @@ -0,0 +1,220 @@ |
1573 | +From 5495c82ec8c1fbbf2f4da962b879366645ca58e7 Mon Sep 17 00:00:00 2001 |
1574 | +From: Frode Nordahl <fnordahl@ubuntu.com> |
1575 | +Date: Thu, 26 Oct 2023 11:50:55 +0200 |
1576 | +Subject: [PATCH 4/4] Add router default route BFD/ECMP options |
1577 | + |
1578 | +From: Frode Nordahl <frode.nordahl@canonical.com> |
1579 | + |
1580 | +Add the `--enable-default-route-bfd`, `--disable-default-route-bfd` |
1581 | +`--enable-default-route-ecmp` and `--disable-default-route-ecmp` |
1582 | +options for `router create` and `router set` commands. |
1583 | + |
1584 | +Related-Bug: #2002687 |
1585 | +Signed-off-by: Frode Nordahl <frode.nordahl@canonical.com> |
1586 | +Change-Id: Ia5a196daa87d29445dc5514dcb91544f9d470795 |
1587 | +(cherry picked from commit 7184e876a5b79ea226c75a267af2dfca7c011f62) |
1588 | + |
1589 | +Origin: upstream, https://opendev.org/openstack/python-openstackclient/commit/7184e876a5b79ea226c75a267af2dfca7c011f62 |
1590 | +Bug: https://launchpad.net/bugs/2002687 |
1591 | +Description: The main feature went into OpenStack Neutron at Caracal, and due |
1592 | + to review capacity the client patches did unfortunately not make it. Enable |
1593 | + the feature for our users while we await the next python-openstackclient |
1594 | + |
1595 | +--- |
1596 | + openstackclient/network/v2/router.py | 69 +++++++++++++++++++ |
1597 | + .../tests/unit/network/v2/test_router.py | 68 ++++++++++++++++++ |
1598 | + 2 files changed, 137 insertions(+) |
1599 | + |
1600 | +diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py |
1601 | +index 52b7d25f..51e2aba4 100644 |
1602 | +--- a/openstackclient/network/v2/router.py |
1603 | ++++ b/openstackclient/network/v2/router.py |
1604 | +@@ -231,9 +231,72 @@ def _get_attrs(client_manager, parsed_args): |
1605 | + if 'flavor_id' in parsed_args and parsed_args.flavor_id is not None: |
1606 | + attrs['flavor_id'] = parsed_args.flavor_id |
1607 | + |
1608 | ++ for attr in ('enable_default_route_bfd', 'enable_default_route_ecmp'): |
1609 | ++ value = getattr(parsed_args, attr, None) |
1610 | ++ if value is not None: |
1611 | ++ attrs[attr] = value |
1612 | ++ |
1613 | + return attrs |
1614 | + |
1615 | + |
1616 | ++def _parser_add_bfd_ecmp_arguments(parser): |
1617 | ++ """Helper to add BFD and ECMP args for CreateRouter and SetRouter.""" |
1618 | ++ parser.add_argument( |
1619 | ++ '--enable-default-route-bfd', |
1620 | ++ dest='enable_default_route_bfd', |
1621 | ++ default=None, |
1622 | ++ action='store_true', |
1623 | ++ help=_( |
1624 | ++ "Enable BFD sessions for default routes inferred from " |
1625 | ++ "the external gateway port subnets for this router." |
1626 | ++ ), |
1627 | ++ ) |
1628 | ++ parser.add_argument( |
1629 | ++ '--disable-default-route-bfd', |
1630 | ++ dest='enable_default_route_bfd', |
1631 | ++ default=None, |
1632 | ++ action='store_false', |
1633 | ++ help=_( |
1634 | ++ "Disable BFD sessions for default routes inferred from " |
1635 | ++ "the external gateway port subnets for this router." |
1636 | ++ ), |
1637 | ++ ) |
1638 | ++ parser.add_argument( |
1639 | ++ '--enable-default-route-ecmp', |
1640 | ++ dest='enable_default_route_ecmp', |
1641 | ++ default=None, |
1642 | ++ action='store_true', |
1643 | ++ help=_( |
1644 | ++ "Add ECMP default routes if multiple are available via " |
1645 | ++ "different gateway ports." |
1646 | ++ ), |
1647 | ++ ) |
1648 | ++ parser.add_argument( |
1649 | ++ '--disable-default-route-ecmp', |
1650 | ++ dest='enable_default_route_ecmp', |
1651 | ++ default=None, |
1652 | ++ action='store_false', |
1653 | ++ help=_("Add default route only for first gateway port."), |
1654 | ++ ) |
1655 | ++ |
1656 | ++ |
1657 | ++def _command_check_bfd_ecmp_supported(attrs, client): |
1658 | ++ """Helper to check for server side support when bfd/ecmp attrs provided. |
1659 | ++ |
1660 | ++ :raises: exceptions.CommandError |
1661 | ++ """ |
1662 | ++ if ( |
1663 | ++ 'enable_default_route_bfd' in attrs |
1664 | ++ or 'enable_default_route_ecmp' in attrs |
1665 | ++ ) and not is_multiple_gateways_supported(client): |
1666 | ++ msg = _( |
1667 | ++ 'The external-gateway-multihoming extension is not enabled at ' |
1668 | ++ 'the Neutron side, cannot use --enable-default-route-bfd or ' |
1669 | ++ '--enable-default-route-ecmp arguments.' |
1670 | ++ ) |
1671 | ++ raise exceptions.CommandError(msg) |
1672 | ++ |
1673 | ++ |
1674 | + class AddPortToRouter(command.Command): |
1675 | + _description = _("Add a port to a router") |
1676 | + |
1677 | +@@ -502,6 +565,7 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): |
1678 | + metavar='<flavor-id>', |
1679 | + help=_("Associate the router to a flavor by ID"), |
1680 | + ) |
1681 | ++ _parser_add_bfd_ecmp_arguments(parser) |
1682 | + |
1683 | + return parser |
1684 | + |
1685 | +@@ -527,6 +591,8 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): |
1686 | + if parsed_args.enable_ndp_proxy is not None: |
1687 | + attrs['enable_ndp_proxy'] = parsed_args.enable_ndp_proxy |
1688 | + |
1689 | ++ _command_check_bfd_ecmp_supported(attrs, client) |
1690 | ++ |
1691 | + external_gateways = attrs.pop('external_gateways', None) |
1692 | + obj = client.create_router(**attrs) |
1693 | + # tags cannot be set when created, so tags need to be set later. |
1694 | +@@ -943,6 +1009,7 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
1695 | + help=_("Remove QoS policy from router gateway IPs"), |
1696 | + ) |
1697 | + _tag.add_tag_option_to_parser_for_set(parser, _('router')) |
1698 | ++ _parser_add_bfd_ecmp_arguments(parser) |
1699 | + return parser |
1700 | + |
1701 | + def take_action(self, parsed_args): |
1702 | +@@ -1015,6 +1082,8 @@ class SetRouter(common.NeutronCommandWithExtraArgs): |
1703 | + if parsed_args.enable_ndp_proxy is not None: |
1704 | + attrs['enable_ndp_proxy'] = parsed_args.enable_ndp_proxy |
1705 | + |
1706 | ++ _command_check_bfd_ecmp_supported(attrs, client) |
1707 | ++ |
1708 | + if attrs: |
1709 | + external_gateways = attrs.pop('external_gateways', None) |
1710 | + client.update_router(obj, **attrs) |
1711 | +diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py |
1712 | +index 33956b3e..256fd9b7 100644 |
1713 | +--- a/openstackclient/tests/unit/network/v2/test_router.py |
1714 | ++++ b/openstackclient/tests/unit/network/v2/test_router.py |
1715 | +@@ -410,6 +410,74 @@ class TestCreateRouter(TestRouter): |
1716 | + self.assertEqual(self.columns, columns) |
1717 | + self.assertCountEqual(self.data, data) |
1718 | + |
1719 | ++ def test_create_with_enable_default_route_bfd(self): |
1720 | ++ self.network_client.find_extension = mock.Mock( |
1721 | ++ return_value=network_fakes.create_one_extension( |
1722 | ++ attrs={'name': 'external-gateway-multihoming'} |
1723 | ++ ) |
1724 | ++ ) |
1725 | ++ arglist = [self.new_router.name, '--enable-default-route-bfd'] |
1726 | ++ verifylist = [ |
1727 | ++ ('name', self.new_router.name), |
1728 | ++ ('enable_default_route_bfd', True), |
1729 | ++ ] |
1730 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1731 | ++ columns, data = self.cmd.take_action(parsed_args) |
1732 | ++ self.network_client.create_router.assert_called_once_with( |
1733 | ++ name=self.new_router.name, |
1734 | ++ admin_state_up=True, |
1735 | ++ enable_default_route_bfd=True, |
1736 | ++ ) |
1737 | ++ self.assertEqual(self.columns, columns) |
1738 | ++ self.assertCountEqual(self.data, data) |
1739 | ++ |
1740 | ++ def test_create_with_enable_default_route_bfd_no_extension(self): |
1741 | ++ arglist = [self.new_router.name, '--enable-default-route-bfd'] |
1742 | ++ verifylist = [ |
1743 | ++ ('name', self.new_router.name), |
1744 | ++ ('enable_default_route_bfd', True), |
1745 | ++ ] |
1746 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1747 | ++ self.assertRaises( |
1748 | ++ exceptions.CommandError, |
1749 | ++ self.cmd.take_action, |
1750 | ++ parsed_args, |
1751 | ++ ) |
1752 | ++ |
1753 | ++ def test_create_with_enable_default_route_ecmp(self): |
1754 | ++ self.network_client.find_extension = mock.Mock( |
1755 | ++ return_value=network_fakes.create_one_extension( |
1756 | ++ attrs={'name': 'external-gateway-multihoming'} |
1757 | ++ ) |
1758 | ++ ) |
1759 | ++ arglist = [self.new_router.name, '--enable-default-route-ecmp'] |
1760 | ++ verifylist = [ |
1761 | ++ ('name', self.new_router.name), |
1762 | ++ ('enable_default_route_ecmp', True), |
1763 | ++ ] |
1764 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1765 | ++ columns, data = self.cmd.take_action(parsed_args) |
1766 | ++ self.network_client.create_router.assert_called_once_with( |
1767 | ++ name=self.new_router.name, |
1768 | ++ admin_state_up=True, |
1769 | ++ enable_default_route_ecmp=True, |
1770 | ++ ) |
1771 | ++ self.assertEqual(self.columns, columns) |
1772 | ++ self.assertCountEqual(self.data, data) |
1773 | ++ |
1774 | ++ def test_create_with_enable_default_route_ecmp_no_extension(self): |
1775 | ++ arglist = [self.new_router.name, '--enable-default-route-ecmp'] |
1776 | ++ verifylist = [ |
1777 | ++ ('name', self.new_router.name), |
1778 | ++ ('enable_default_route_ecmp', True), |
1779 | ++ ] |
1780 | ++ parsed_args = self.check_parser(self.cmd, arglist, verifylist) |
1781 | ++ self.assertRaises( |
1782 | ++ exceptions.CommandError, |
1783 | ++ self.cmd.take_action, |
1784 | ++ parsed_args, |
1785 | ++ ) |
1786 | ++ |
1787 | + |
1788 | + class TestDeleteRouter(TestRouter): |
1789 | + # The routers to delete. |
1790 | +-- |
1791 | +2.43.0 |
1792 | + |
1793 | diff --git a/debian/patches/series b/debian/patches/series |
1794 | new file mode 100644 |
1795 | index 0000000..91772f3 |
1796 | --- /dev/null |
1797 | +++ b/debian/patches/series |
1798 | @@ -0,0 +1,4 @@ |
1799 | +lp-2002687-1-Parse-external-gateway-argument-in-separate-helper.patch |
1800 | +lp-2002687-2-router-Use-plural-form-for-storage-of-fixed_ip-argum.patch |
1801 | +lp-2002687-3-Add-support-for-managing-external-gateways.patch |
1802 | +lp-2002687-4-Add-router-default-route-BFD-ECMP-options.patch |