@@ -36,9 +36,21 @@ def _format_security_group_rule_show(obj):
3636
3737
3838def _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+
6487class 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 ,
0 commit comments