Skip to content

Commit 5b14433

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Support JSON data for port binding profile"
2 parents 8b70f31 + 5cc62d9 commit 5b14433

4 files changed

Lines changed: 136 additions & 8 deletions

File tree

doc/source/command-objects/port.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ Create new port
5454
5555
.. option:: --binding-profile <binding-profile>
5656
57-
Custom data to be passed as binding:profile: <key>=<value>
57+
Custom data to be passed as binding:profile. Data may
58+
be passed as <key>=<value> or JSON.
5859
(repeat option to set multiple binding:profile data)
5960
6061
.. option:: --host <host-id>
@@ -162,7 +163,8 @@ Set port properties
162163
163164
.. option:: --binding-profile <binding-profile>
164165
165-
Custom data to be passed as binding:profile: <key>=<value>
166+
Custom data to be passed as binding:profile. Data may
167+
be passed as <key>=<value> or JSON.
166168
(repeat option to set multiple binding:profile data)
167169
168170
.. option:: --no-binding-profile

openstackclient/network/v2/port.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Port action implementations"""
1515

1616
import argparse
17+
import json
1718
import logging
1819

1920
from osc_lib.cli import parseractions
@@ -63,6 +64,32 @@ def _get_columns(item):
6364
return tuple(sorted(columns))
6465

6566

67+
class JSONKeyValueAction(argparse.Action):
68+
"""A custom action to parse arguments as JSON or key=value pairs
69+
70+
Ensures that ``dest`` is a dict
71+
"""
72+
73+
def __call__(self, parser, namespace, values, option_string=None):
74+
75+
# Make sure we have an empty dict rather than None
76+
if getattr(namespace, self.dest, None) is None:
77+
setattr(namespace, self.dest, {})
78+
79+
# Try to load JSON first before falling back to <key>=<value>.
80+
current_dest = getattr(namespace, self.dest)
81+
try:
82+
current_dest.update(json.loads(values))
83+
except ValueError as e:
84+
if '=' in values:
85+
current_dest.update([values.split('=', 1)])
86+
else:
87+
msg = _("Expected '<key>=<value>' or JSON data for option "
88+
"%(option)s, but encountered JSON parsing error: "
89+
"%(error)s") % {"option": option_string, "error": e}
90+
raise argparse.ArgumentTypeError(msg)
91+
92+
6693
def _get_attrs(client_manager, parsed_args):
6794
attrs = {}
6895

@@ -219,9 +246,9 @@ def get_parser(self, prog_name):
219246
parser.add_argument(
220247
'--binding-profile',
221248
metavar='<binding-profile>',
222-
action=parseractions.KeyValueAction,
223-
help=_("Custom data to be passed as binding:profile: "
224-
"<key>=<value> "
249+
action=JSONKeyValueAction,
250+
help=_("Custom data to be passed as binding:profile. Data may "
251+
"be passed as <key>=<value> or JSON. "
225252
"(repeat option to set multiple binding:profile data)")
226253
)
227254
admin_group = parser.add_mutually_exclusive_group()
@@ -390,9 +417,9 @@ def get_parser(self, prog_name):
390417
binding_profile.add_argument(
391418
'--binding-profile',
392419
metavar='<binding-profile>',
393-
action=parseractions.KeyValueAction,
394-
help=_("Custom data to be passed as binding:profile: "
395-
"<key>=<value> "
420+
action=JSONKeyValueAction,
421+
help=_("Custom data to be passed as binding:profile. Data may "
422+
"be passed as <key>=<value> or JSON. "
396423
"(repeat option to set multiple binding:profile data)")
397424
)
398425
binding_profile.add_argument(

openstackclient/tests/network/v2/test_port.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# under the License.
1212
#
1313

14+
import argparse
1415
import mock
1516

1617
from mock import call
@@ -174,6 +175,58 @@ def test_create_full_options(self):
174175
self.assertEqual(ref_columns, columns)
175176
self.assertEqual(ref_data, data)
176177

178+
def test_create_invalid_json_binding_profile(self):
179+
arglist = [
180+
'--network', self._port.network_id,
181+
'--binding-profile', '{"parent_name":"fake_parent"',
182+
'test-port',
183+
]
184+
self.assertRaises(argparse.ArgumentTypeError,
185+
self.check_parser,
186+
self.cmd,
187+
arglist,
188+
None)
189+
190+
def test_create_invalid_key_value_binding_profile(self):
191+
arglist = [
192+
'--network', self._port.network_id,
193+
'--binding-profile', 'key',
194+
'test-port',
195+
]
196+
self.assertRaises(argparse.ArgumentTypeError,
197+
self.check_parser,
198+
self.cmd,
199+
arglist,
200+
None)
201+
202+
def test_create_json_binding_profile(self):
203+
arglist = [
204+
'--network', self._port.network_id,
205+
'--binding-profile', '{"parent_name":"fake_parent"}',
206+
'--binding-profile', '{"tag":42}',
207+
'test-port',
208+
]
209+
verifylist = [
210+
('network', self._port.network_id,),
211+
('enable', True),
212+
('binding_profile', {'parent_name': 'fake_parent', 'tag': 42}),
213+
('name', 'test-port'),
214+
]
215+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
216+
217+
columns, data = (self.cmd.take_action(parsed_args))
218+
219+
self.network.create_port.assert_called_once_with(**{
220+
'admin_state_up': True,
221+
'network_id': self._port.network_id,
222+
'binding:profile': {'parent_name': 'fake_parent', 'tag': 42},
223+
'name': 'test-port',
224+
})
225+
226+
ref_columns, ref_data = self._get_common_cols_data(self._port)
227+
self.assertEqual(ref_columns, columns)
228+
self.assertEqual(ref_data, data)
229+
177230

178231
class TestDeletePort(TestPort):
179232

@@ -442,6 +495,48 @@ def test_set_nothing(self):
442495
self.network.update_port.assert_called_once_with(self._port, **attrs)
443496
self.assertIsNone(result)
444497

498+
def test_set_invalid_json_binding_profile(self):
499+
arglist = [
500+
'--binding-profile', '{"parent_name"}',
501+
'test-port',
502+
]
503+
self.assertRaises(argparse.ArgumentTypeError,
504+
self.check_parser,
505+
self.cmd,
506+
arglist,
507+
None)
508+
509+
def test_set_invalid_key_value_binding_profile(self):
510+
arglist = [
511+
'--binding-profile', 'key',
512+
'test-port',
513+
]
514+
self.assertRaises(argparse.ArgumentTypeError,
515+
self.check_parser,
516+
self.cmd,
517+
arglist,
518+
None)
519+
520+
def test_set_mixed_binding_profile(self):
521+
arglist = [
522+
'--binding-profile', 'foo=bar',
523+
'--binding-profile', '{"foo2": "bar2"}',
524+
self._port.name,
525+
]
526+
verifylist = [
527+
('binding_profile', {'foo': 'bar', 'foo2': 'bar2'}),
528+
('port', self._port.name),
529+
]
530+
531+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
532+
result = self.cmd.take_action(parsed_args)
533+
534+
attrs = {
535+
'binding:profile': {'foo': 'bar', 'foo2': 'bar2'},
536+
}
537+
self.network.update_port.assert_called_once_with(self._port, **attrs)
538+
self.assertIsNone(result)
539+
445540

446541
class TestShowPort(TestPort):
447542

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
---
22
features:
3+
- Update ``--binding-profile`` option on the ``port create`` and
4+
``port set`` commands to support JSON input for more advanced
5+
binding profile data.
6+
[Blueprint :oscbp:`neutron-client`]
37
- Add ``geneve`` choice to the ``network create`` command
48
``--provider-network-type`` option.
59
[Blueprint :oscbp:`neutron-client`]

0 commit comments

Comments
 (0)