Skip to content

Commit 6ab9e56

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Additional network protocol support"
2 parents 7a25212 + fd5fd92 commit 6ab9e56

6 files changed

Lines changed: 444 additions & 54 deletions

File tree

doc/source/command-objects/security-group-rule.rst

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,14 @@ Create a new security group rule
1616
.. code:: bash
1717
1818
os security group rule create
19-
[--proto <proto>]
2019
[--src-ip <ip-address> | --src-group <group>]
21-
[--dst-port <port-range>]
20+
[--dst-port <port-range> | [--icmp-type <icmp-type> [--icmp-code <icmp-code>]]]
21+
[--protocol <protocol>]
2222
[--ingress | --egress]
2323
[--ethertype <ethertype>]
2424
[--project <project> [--project-domain <project-domain>]]
2525
<group>
2626
27-
.. option:: --proto <proto>
28-
29-
IP protocol (icmp, tcp, udp; default: tcp)
30-
3127
.. option:: --src-ip <ip-address>
3228
3329
Source IP address block
@@ -39,8 +35,35 @@ Create a new security group rule
3935
4036
.. option:: --dst-port <port-range>
4137
42-
Destination port, may be a single port or port range: 137:139
43-
(only required for IP protocols tcp and udp)
38+
Destination port, may be a single port or a starting and
39+
ending port range: 137:139. Required for IP protocols TCP
40+
and UDP. Ignored for ICMP IP protocols.
41+
42+
.. option:: --icmp-type <icmp-type>
43+
44+
ICMP type for ICMP IP protocols
45+
46+
*Network version 2 only*
47+
48+
.. option:: --icmp-code <icmp-code>
49+
50+
ICMP code for ICMP IP protocols
51+
52+
*Network version 2 only*
53+
54+
.. option:: --protocol <protocol>
55+
56+
IP protocol (icmp, tcp, udp; default: tcp)
57+
58+
*Compute version 2*
59+
60+
IP protocol (ah, dccp, egp, esp, gre, icmp, igmp,
61+
ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt,
62+
ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp,
63+
udp, udplite, vrrp and integer representations [0-255];
64+
default: tcp)
65+
66+
*Network version 2*
4467
4568
.. option:: --ingress
4669
@@ -56,7 +79,8 @@ Create a new security group rule
5679
5780
.. option:: --ethertype <ethertype>
5881
59-
Ethertype of network traffic (IPv4, IPv6; default: IPv4)
82+
Ethertype of network traffic
83+
(IPv4, IPv6; default: based on IP protocol)
6084
6185
*Network version 2 only*
6286

functional/tests/network/v2/test_security_group_rule.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def setUpClass(cls):
3737
opts = cls.get_show_opts(cls.ID_FIELD)
3838
raw_output = cls.openstack('security group rule create ' +
3939
cls.SECURITY_GROUP_NAME +
40-
' --proto tcp --dst-port 80:80' +
40+
' --protocol tcp --dst-port 80:80' +
4141
' --ingress --ethertype IPv4' +
4242
opts)
4343
cls.SECURITY_GROUP_RULE_ID = raw_output.strip('\n')

openstackclient/network/v2/security_group_rule.py

Lines changed: 152 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,21 @@ def _format_security_group_rule_show(obj):
3636

3737

3838
def _format_network_port_range(rule):
39+
# Display port range or ICMP type and code. For example:
40+
# - ICMP type: 'type=3'
41+
# - ICMP type and code: 'type=3:code=0'
42+
# - ICMP code: Not supported
43+
# - Matching port range: '443:443'
44+
# - Different port range: '22:24'
45+
# - Single port: '80:80'
46+
# - No port range: ''
3947
port_range = ''
40-
if (rule.protocol != 'icmp' and
41-
(rule.port_range_min or rule.port_range_max)):
48+
if _is_icmp_protocol(rule.protocol):
49+
if rule.port_range_min:
50+
port_range += 'type=' + str(rule.port_range_min)
51+
if rule.port_range_max:
52+
port_range += ':code=' + str(rule.port_range_max)
53+
elif rule.port_range_min or rule.port_range_max:
4254
port_range_min = str(rule.port_range_min)
4355
port_range_max = str(rule.port_range_max)
4456
if rule.port_range_min is None:
@@ -61,26 +73,25 @@ def _convert_to_lowercase(string):
6173
return string.lower()
6274

6375

76+
def _is_icmp_protocol(protocol):
77+
# NOTE(rtheis): Neutron has deprecated protocol icmpv6.
78+
# However, while the OSC CLI doesn't document the protocol,
79+
# the code must still handle it. In addition, handle both
80+
# protocol names and numbers.
81+
if protocol in ['icmp', 'icmpv6', 'ipv6-icmp', '1', '58']:
82+
return True
83+
else:
84+
return False
85+
86+
6487
class CreateSecurityGroupRule(common.NetworkAndComputeShowOne):
6588
"""Create a new security group rule"""
6689

6790
def update_parser_common(self, parser):
6891
parser.add_argument(
6992
'group',
7093
metavar='<group>',
71-
help='Create rule in this security group (name or ID)',
72-
)
73-
# TODO(rtheis): Add support for additional protocols for network.
74-
# Until then, continue enforcing the compute choices. When additional
75-
# protocols are added, the default ethertype must be determined
76-
# based on the protocol.
77-
parser.add_argument(
78-
"--proto",
79-
metavar="<proto>",
80-
default="tcp",
81-
choices=['icmp', 'tcp', 'udp'],
82-
type=_convert_to_lowercase,
83-
help=_("IP protocol (icmp, tcp, udp; default: tcp)")
94+
help=_("Create rule in this security group (name or ID)")
8495
)
8596
source_group = parser.add_mutually_exclusive_group()
8697
source_group.add_argument(
@@ -94,17 +105,49 @@ def update_parser_common(self, parser):
94105
metavar="<group>",
95106
help=_("Source security group (name or ID)")
96107
)
97-
parser.add_argument(
98-
"--dst-port",
99-
metavar="<port-range>",
100-
default=(0, 0),
101-
action=parseractions.RangeAction,
102-
help=_("Destination port, may be a single port or port range: "
103-
"137:139 (only required for IP protocols tcp and udp)")
104-
)
105108
return parser
106109

107110
def update_parser_network(self, parser):
111+
parser.add_argument(
112+
'--dst-port',
113+
metavar='<port-range>',
114+
action=parseractions.RangeAction,
115+
help=_("Destination port, may be a single port or a starting and "
116+
"ending port range: 137:139. Required for IP protocols TCP "
117+
"and UDP. Ignored for ICMP IP protocols.")
118+
)
119+
parser.add_argument(
120+
'--icmp-type',
121+
metavar='<icmp-type>',
122+
type=int,
123+
help=_("ICMP type for ICMP IP protocols")
124+
)
125+
parser.add_argument(
126+
'--icmp-code',
127+
metavar='<icmp-code>',
128+
type=int,
129+
help=_("ICMP code for ICMP IP protocols")
130+
)
131+
# NOTE(rtheis): Support either protocol option name for now.
132+
# However, consider deprecating and then removing --proto in
133+
# a future release.
134+
protocol_group = parser.add_mutually_exclusive_group()
135+
protocol_group.add_argument(
136+
'--protocol',
137+
metavar='<protocol>',
138+
type=_convert_to_lowercase,
139+
help=_("IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, "
140+
"ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, "
141+
"ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, "
142+
"udp, udplite, vrrp and integer representations [0-255]; "
143+
"default: tcp)")
144+
)
145+
protocol_group.add_argument(
146+
'--proto',
147+
metavar='<proto>',
148+
type=_convert_to_lowercase,
149+
help=argparse.SUPPRESS
150+
)
108151
direction_group = parser.add_mutually_exclusive_group()
109152
direction_group.add_argument(
110153
'--ingress',
@@ -120,7 +163,8 @@ def update_parser_network(self, parser):
120163
'--ethertype',
121164
metavar='<ethertype>',
122165
choices=['IPv4', 'IPv6'],
123-
help=_("Ethertype of network traffic (IPv4, IPv6; default: IPv4)")
166+
help=_("Ethertype of network traffic "
167+
"(IPv4, IPv6; default: based on IP protocol)")
124168
)
125169
parser.add_argument(
126170
'--project',
@@ -130,6 +174,55 @@ def update_parser_network(self, parser):
130174
identity_common.add_project_domain_option_to_parser(parser)
131175
return parser
132176

177+
def update_parser_compute(self, parser):
178+
parser.add_argument(
179+
'--dst-port',
180+
metavar='<port-range>',
181+
default=(0, 0),
182+
action=parseractions.RangeAction,
183+
help=_("Destination port, may be a single port or a starting and "
184+
"ending port range: 137:139. Required for IP protocols TCP "
185+
"and UDP. Ignored for ICMP IP protocols.")
186+
)
187+
# NOTE(rtheis): Support either protocol option name for now.
188+
# However, consider deprecating and then removing --proto in
189+
# a future release.
190+
protocol_group = parser.add_mutually_exclusive_group()
191+
protocol_group.add_argument(
192+
'--protocol',
193+
metavar='<protocol>',
194+
choices=['icmp', 'tcp', 'udp'],
195+
type=_convert_to_lowercase,
196+
help=_("IP protocol (icmp, tcp, udp; default: tcp)")
197+
)
198+
protocol_group.add_argument(
199+
'--proto',
200+
metavar='<proto>',
201+
choices=['icmp', 'tcp', 'udp'],
202+
type=_convert_to_lowercase,
203+
help=argparse.SUPPRESS
204+
)
205+
return parser
206+
207+
def _get_protocol(self, parsed_args):
208+
protocol = 'tcp'
209+
if parsed_args.protocol is not None:
210+
protocol = parsed_args.protocol
211+
if parsed_args.proto is not None:
212+
protocol = parsed_args.proto
213+
return protocol
214+
215+
def _is_ipv6_protocol(self, protocol):
216+
# NOTE(rtheis): Neutron has deprecated protocol icmpv6.
217+
# However, while the OSC CLI doesn't document the protocol,
218+
# the code must still handle it. In addition, handle both
219+
# protocol names and numbers.
220+
if (protocol.startswith('ipv6-') or
221+
protocol in ['icmpv6', '41', '43', '44', '58', '59', '60']):
222+
return True
223+
else:
224+
return False
225+
133226
def take_action_network(self, client, parsed_args):
134227
# Get the security group ID to hold the rule.
135228
security_group_id = client.find_security_group(
@@ -139,24 +232,50 @@ def take_action_network(self, client, parsed_args):
139232

140233
# Build the create attributes.
141234
attrs = {}
235+
attrs['protocol'] = self._get_protocol(parsed_args)
236+
142237
# NOTE(rtheis): A direction must be specified and ingress
143238
# is the default.
144239
if parsed_args.ingress or not parsed_args.egress:
145240
attrs['direction'] = 'ingress'
146241
if parsed_args.egress:
147242
attrs['direction'] = 'egress'
243+
244+
# NOTE(rtheis): Use ethertype specified else default based
245+
# on IP protocol.
148246
if parsed_args.ethertype:
149247
attrs['ethertype'] = parsed_args.ethertype
248+
elif self._is_ipv6_protocol(attrs['protocol']):
249+
attrs['ethertype'] = 'IPv6'
150250
else:
151-
# NOTE(rtheis): Default based on protocol is IPv4 for now.
152-
# Once IPv6 protocols are added, this will need to be updated.
153251
attrs['ethertype'] = 'IPv4'
154-
# TODO(rtheis): Add port range support (type and code) for icmp
155-
# protocol. Until then, continue ignoring the port range.
156-
if parsed_args.proto != 'icmp':
252+
253+
# NOTE(rtheis): Validate the port range and ICMP type and code.
254+
# It would be ideal if argparse could do this.
255+
if parsed_args.dst_port and (parsed_args.icmp_type or
256+
parsed_args.icmp_code):
257+
msg = _('Argument --dst-port not allowed with arguments '
258+
'--icmp-type and --icmp-code')
259+
raise exceptions.CommandError(msg)
260+
if parsed_args.icmp_type is None and parsed_args.icmp_code is not None:
261+
msg = _('Argument --icmp-type required with argument --icmp-code')
262+
raise exceptions.CommandError(msg)
263+
is_icmp_protocol = _is_icmp_protocol(attrs['protocol'])
264+
if not is_icmp_protocol and (parsed_args.icmp_type or
265+
parsed_args.icmp_code):
266+
msg = _('ICMP IP protocol required with arguments '
267+
'--icmp-type and --icmp-code')
268+
raise exceptions.CommandError(msg)
269+
# NOTE(rtheis): For backwards compatibility, continue ignoring
270+
# the destination port range when an ICMP IP protocol is specified.
271+
if parsed_args.dst_port and not is_icmp_protocol:
157272
attrs['port_range_min'] = parsed_args.dst_port[0]
158273
attrs['port_range_max'] = parsed_args.dst_port[1]
159-
attrs['protocol'] = parsed_args.proto
274+
if parsed_args.icmp_type:
275+
attrs['port_range_min'] = parsed_args.icmp_type
276+
if parsed_args.icmp_code:
277+
attrs['port_range_max'] = parsed_args.icmp_code
278+
160279
if parsed_args.src_group is not None:
161280
attrs['remote_group_id'] = client.find_security_group(
162281
parsed_args.src_group,
@@ -187,7 +306,8 @@ def take_action_compute(self, client, parsed_args):
187306
client.security_groups,
188307
parsed_args.group,
189308
)
190-
if parsed_args.proto == 'icmp':
309+
protocol = self._get_protocol(parsed_args)
310+
if protocol == 'icmp':
191311
from_port, to_port = -1, -1
192312
else:
193313
from_port, to_port = parsed_args.dst_port
@@ -203,7 +323,7 @@ def take_action_compute(self, client, parsed_args):
203323
src_ip = '0.0.0.0/0'
204324
obj = client.security_group_rules.create(
205325
group.id,
206-
parsed_args.proto,
326+
protocol,
207327
from_port,
208328
to_port,
209329
src_ip,

openstackclient/tests/network/v2/fakes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,8 +496,8 @@ def create_one_security_group_rule(attrs=None):
496496
'direction': 'ingress',
497497
'ethertype': 'IPv4',
498498
'id': 'security-group-rule-id-' + uuid.uuid4().hex,
499-
'port_range_max': 0,
500-
'port_range_min': 0,
499+
'port_range_max': None,
500+
'port_range_min': None,
501501
'protocol': 'tcp',
502502
'remote_group_id': None,
503503
'remote_ip_prefix': '0.0.0.0/0',

0 commit comments

Comments
 (0)