Skip to content

Commit 5cc62d9

Browse files
committed
Support JSON data for port binding profile
Update the "--binding-profile" option on the "port create" and "port set" commands to support both <key>=<value> and JSON input for the port custom binding profile data. The JSON input is sometimes needed to maintain the value type (e.g. integer) for more advanced data. The port custom binding profile data is unique across neutron so a custom argparse.Action class was created instead of writting a generic class in osc-lib. Change-Id: I82ac6d4f95afdc866f5282fc00d390f850f54d21 Implements: blueprint neutron-client
1 parent 40004b5 commit 5cc62d9

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)