Skip to content

Commit 83e3006

Browse files
committed
Add private dnat rules create endpoint
1 parent 9fb77e7 commit 83e3006

10 files changed

Lines changed: 301 additions & 3 deletions

File tree

doc/source/sdk/guides/privatenat.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,18 @@ the IP address mapping and port mapping.
7171
List Private DNAT Rules
7272
^^^^^^^^^^^^^^^^^^^^^^^
7373

74-
This interface is used to query an DNAT rule list and to filter
74+
This interface is used to query a DNAT rule list and to filter
7575
the output with query parameters.
7676
:class:`~otcextensions.sdk.natv3.v3.dnat.PrivateDnat`.
7777

7878
.. literalinclude:: ../examples/natv3/list_private_dnat_rules.py
7979
:lines: 16-23
80+
81+
Create Private DNAT Rules
82+
^^^^^^^^^^^^^^^^^^^^^^^^^
83+
84+
This interface is used to create a DNAT rule.
85+
:class:`~otcextensions.sdk.natv3.v3.dnat.PrivateDnat`.
86+
87+
.. literalinclude:: ../examples/natv3/create_private_dnat_rules.py
88+
:lines: 16-32
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env python3
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain
4+
# a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
# License for the specific language governing permissions and limitations
12+
# under the License.
13+
"""
14+
Create a Private DNAT Rule
15+
"""
16+
17+
import openstack
18+
19+
openstack.enable_logging(True)
20+
conn = openstack.connect(cloud="otc")
21+
22+
dnat_rule_attrs = {
23+
"gateway_id": "0adefb29-a6c2-48a5-8637-2be67fa03fec",
24+
"transit_ip_id": "3faa719d-6d18-4ccb-a5c7-33e65a09663e",
25+
"network_interface_id": "dae9393a-b536-491c-a5a2-72edc1104707",
26+
"protocol": "tcp",
27+
"internal_service_port": 22,
28+
"transit_service_port": 22,
29+
}
30+
31+
dnat_rule = conn.natv3.create_private_dnat_rule(**dnat_rule_attrs)
32+
print(dnat_rule)

otcextensions/osclient/privatenat/v3/dnat.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@
1414

1515
import logging
1616

17+
from osc_lib import exceptions
1718
from osc_lib import utils
1819
from osc_lib.command import command
1920

21+
from otcextensions.common import sdk_utils
2022
from otcextensions.i18n import _
2123

2224
LOG = logging.getLogger(__name__)
2325

2426

27+
def _get_columns(item):
28+
column_map = {}
29+
return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map)
30+
31+
2532
class ListPrivateDnatRules(command.Lister):
2633

2734
_description = _("List private DNAT rules.")
@@ -240,3 +247,123 @@ def take_action(self, parsed_args):
240247
for s in data
241248
),
242249
)
250+
251+
252+
class CreatePrivateDnatRule(command.ShowOne):
253+
254+
_description = _("Create a private DNAT rule.")
255+
256+
def get_parser(self, prog_name):
257+
parser = super(CreatePrivateDnatRule, self).get_parser(prog_name)
258+
259+
parser.add_argument(
260+
"--gateway-id",
261+
metavar="<gateway_id>",
262+
required=True,
263+
help=_("Specifies the private NAT gateway ID."),
264+
)
265+
parser.add_argument(
266+
"--transit-ip-id",
267+
metavar="<transit_ip_id>",
268+
required=True,
269+
help=_("Specifies the transit IP address ID."),
270+
)
271+
parser.add_argument(
272+
"--type",
273+
metavar="<type>",
274+
choices=["COMPUTE", "VIP", "ELB", "ELBv3", "CUSTOMIZE"],
275+
help=_(
276+
"Specifies the backend resource type. "
277+
"Supported values: COMPUTE, VIP, ELB, ELBv3, CUSTOMIZE."
278+
),
279+
)
280+
parser.add_argument(
281+
"--network-interface-id",
282+
metavar="<network_interface_id>",
283+
help=_("Specifies the port ID of the backend resource."),
284+
)
285+
parser.add_argument(
286+
"--private-ip-address",
287+
metavar="<private_ip_address>",
288+
help=_("Specifies the backend private IP address."),
289+
)
290+
parser.add_argument(
291+
"--protocol",
292+
metavar="<protocol>",
293+
choices=["tcp", "udp", "any", "TCP", "UDP", "ANY"],
294+
help=_("Specifies the protocol type. " "Supported values: tcp, udp, any."),
295+
)
296+
parser.add_argument(
297+
"--internal-service-port",
298+
metavar="<internal_service_port>",
299+
type=int,
300+
help=_("Specifies the port number of the backend resource."),
301+
)
302+
parser.add_argument(
303+
"--transit-service-port",
304+
metavar="<transit_service_port>",
305+
type=int,
306+
help=_("Specifies the port number of the transit IP address."),
307+
)
308+
parser.add_argument(
309+
"--description",
310+
metavar="<description>",
311+
help=_("Provides supplementary information about the DNAT rule."),
312+
)
313+
parser.add_argument(
314+
"--enterprise-project-id",
315+
metavar="<enterprise_project_id>",
316+
help=_("Specifies the enterprise project ID."),
317+
)
318+
return parser
319+
320+
def _build_attrs(self, parsed_args):
321+
attrs = {
322+
"gateway_id": parsed_args.gateway_id,
323+
"transit_ip_id": parsed_args.transit_ip_id,
324+
}
325+
optional_attrs = (
326+
"type",
327+
"protocol",
328+
"network_interface_id",
329+
"private_ip_address",
330+
"internal_service_port",
331+
"transit_service_port",
332+
"description",
333+
"enterprise_project_id",
334+
)
335+
336+
if parsed_args.network_interface_id and parsed_args.private_ip_address:
337+
raise exceptions.CommandError(
338+
_(
339+
"Specify either --network-interface-id or "
340+
"--private-ip-address, but not both."
341+
)
342+
)
343+
344+
if not parsed_args.network_interface_id and not parsed_args.private_ip_address:
345+
raise exceptions.CommandError(
346+
_(
347+
"One of --network-interface-id or --private-ip-address "
348+
"must be specified."
349+
)
350+
)
351+
352+
for key in optional_attrs:
353+
value = getattr(parsed_args, key, None)
354+
if value is not None:
355+
if key == "protocol":
356+
attrs["protocol"] = value.lower()
357+
else:
358+
attrs[key] = value
359+
360+
return attrs
361+
362+
def take_action(self, parsed_args):
363+
client = self.app.client_manager.privatenat
364+
attrs = self._build_attrs(parsed_args)
365+
obj = client.create_private_dnat_rule(**attrs)
366+
367+
display_columns, columns = _get_columns(obj)
368+
data = utils.get_item_properties(obj, columns)
369+
return display_columns, data

otcextensions/sdk/natv3/v3/_proxy.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,16 @@ def private_dnat_rules(self, **query):
106106
:returns: A generator of :class:`~otcextensions.sdk.nat_gateway.v3.dnat.Dnat`
107107
"""
108108
return self._list(_dnat.PrivateDnat, **query)
109+
110+
def create_private_dnat_rule(self, **attrs):
111+
"""Create a private DNAT rule from attributes.
112+
113+
:param dict attrs: Keyword arguments which will be used to create
114+
a :class:`~otcextensions.sdk.natv3.v3.dnat.PrivateDnat`,
115+
comprised of the properties on the PrivateDnat class.
116+
117+
:returns: The result of private DNAT rule creation.
118+
119+
:rtype: :class:`~otcextensions.sdk.natv3.v3.dnat.PrivateDnat`
120+
"""
121+
return self._create(_dnat.PrivateDnat, **attrs)

otcextensions/sdk/natv3/v3/dnat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class PrivateDnat(resource.Resource):
1818
base_path = "/private-nat/dnat-rules"
1919

2020
# capabilities
21+
allow_create = True
2122
allow_list = True
2223

2324
_query_mapping = resource.QueryParameters(

otcextensions/tests/unit/osclient/privatenat/v3/fakes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ def generate(cls):
8585
"description": "test_dnat_rule_description",
8686
"gateway_id": "private-gw-id-" + uuid.uuid4().hex,
8787
"transit_ip_id": uuid.uuid4().hex,
88-
"enterprise_project_id": "2759da7b-8015-404c-ae0a-a389007b0e2a",
89-
"network_interface_id": "dae9393a-b536-491c-a5a2-72edc1104707",
88+
"enterprise_project_id": "ep-" + uuid.uuid4().hex,
89+
"network_interface_id": "net-" + uuid.uuid4().hex,
9090
"type": "COMPUTE",
9191
"protocol": "any",
9292
"internal_service_port": 0,

otcextensions/tests/unit/osclient/privatenat/v3/test_dnat.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
from unittest import mock
1414

15+
from osc_lib import exceptions
16+
1517
from otcextensions.osclient.privatenat.v3 import dnat
1618
from otcextensions.tests.unit.osclient.privatenat.v3 import fakes
1719

@@ -158,3 +160,105 @@ def test_list_with_supported_filters(self):
158160
)
159161
self.assertEqual(self.columns, columns)
160162
self.assertEqual(self.data, list(data))
163+
164+
165+
class TestCreatePrivateDnatRule(fakes.TestPrivateNat):
166+
_data = fakes.FakePrivateDnatRule.create_one()
167+
168+
def setUp(self):
169+
super(TestCreatePrivateDnatRule, self).setUp()
170+
self.cmd = dnat.CreatePrivateDnatRule(self.app, None)
171+
self.client.create_private_dnat_rule = mock.Mock(return_value=self._data)
172+
173+
def test_create_with_network_interface(self):
174+
arglist = [
175+
"--gateway-id",
176+
self._data.gateway_id,
177+
"--transit-ip-id",
178+
self._data.transit_ip_id,
179+
"--type",
180+
"COMPUTE",
181+
"--network-interface-id",
182+
self._data.network_interface_id,
183+
"--protocol",
184+
"TCP",
185+
"--internal-service-port",
186+
"80",
187+
"--transit-service-port",
188+
"8080",
189+
"--description",
190+
self._data.description,
191+
"--enterprise-project-id",
192+
self._data.enterprise_project_id,
193+
]
194+
verifylist = [
195+
("gateway_id", self._data.gateway_id),
196+
("transit_ip_id", self._data.transit_ip_id),
197+
("type", "COMPUTE"),
198+
("network_interface_id", self._data.network_interface_id),
199+
("protocol", "TCP"),
200+
("internal_service_port", 80),
201+
("transit_service_port", 8080),
202+
("description", self._data.description),
203+
("enterprise_project_id", self._data.enterprise_project_id),
204+
]
205+
206+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
207+
columns, data = self.cmd.take_action(parsed_args)
208+
209+
self.client.create_private_dnat_rule.assert_called_once_with(
210+
gateway_id=self._data.gateway_id,
211+
transit_ip_id=self._data.transit_ip_id,
212+
type="COMPUTE",
213+
network_interface_id=self._data.network_interface_id,
214+
protocol="tcp",
215+
internal_service_port=80,
216+
transit_service_port=8080,
217+
description=self._data.description,
218+
enterprise_project_id=self._data.enterprise_project_id,
219+
)
220+
self.assertEqual(len(columns), len(data))
221+
self.assertIn("id", columns)
222+
223+
def test_create_requires_interface_or_private_ip(self):
224+
arglist = [
225+
"--gateway-id",
226+
self._data.gateway_id,
227+
"--transit-ip-id",
228+
self._data.transit_ip_id,
229+
]
230+
verifylist = [
231+
("gateway_id", self._data.gateway_id),
232+
("transit_ip_id", self._data.transit_ip_id),
233+
]
234+
235+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
236+
237+
with self.assertRaises(exceptions.CommandError):
238+
self.cmd.take_action(parsed_args)
239+
240+
def test_create_rejects_mixed_args(self):
241+
arglist = [
242+
"--gateway-id",
243+
self._data.gateway_id,
244+
"--transit-ip-id",
245+
self._data.transit_ip_id,
246+
"--type",
247+
"COMPUTE",
248+
"--network-interface-id",
249+
self._data.network_interface_id,
250+
"--private-ip-address",
251+
self._data.private_ip_address,
252+
]
253+
verifylist = [
254+
("gateway_id", self._data.gateway_id),
255+
("transit_ip_id", self._data.transit_ip_id),
256+
("type", "COMPUTE"),
257+
("network_interface_id", self._data.network_interface_id),
258+
("private_ip_address", self._data.private_ip_address),
259+
]
260+
261+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
262+
263+
with self.assertRaises(exceptions.CommandError):
264+
self.cmd.take_action(parsed_args)

otcextensions/tests/unit/sdk/natv3/v3/test_dnat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def test_basic(self):
4141
self.assertEqual("dnat_rule", sot.resource_key)
4242
self.assertEqual("dnat_rules", sot.resources_key)
4343
self.assertEqual("/private-nat/dnat-rules", sot.base_path)
44+
self.assertTrue(sot.allow_create)
4445
self.assertTrue(sot.allow_list)
4546

4647
def test_make_it(self):

otcextensions/tests/unit/sdk/natv3/v3/test_proxy.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from openstack.tests.unit import test_proxy_base
1414
from otcextensions.sdk.natv3.v3 import _proxy
15+
from otcextensions.sdk.natv3.v3 import dnat
1516
from otcextensions.sdk.natv3.v3 import gateway
1617

1718

@@ -43,3 +44,12 @@ def test_delete_private_nat_gateway(self):
4344
self.verify_delete(
4445
self.proxy.delete_private_nat_gateway, gateway.PrivateNatGateway
4546
)
47+
48+
49+
class TestPrivateDnat(TestNatv3Proxy):
50+
51+
def test_private_dnat_rules(self):
52+
self.verify_list(self.proxy.private_dnat_rules, dnat.PrivateDnat)
53+
54+
def test_create_private_dnat_rule(self):
55+
self.verify_create(self.proxy.create_private_dnat_rule, dnat.PrivateDnat)

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ openstack.privatenat.v3 =
187187
privatenat_gateway_update = otcextensions.osclient.privatenat.v3.private_nat_gateway:UpdatePrivateNatGateway
188188
privatenat_gateway_delete = otcextensions.osclient.privatenat.v3.private_nat_gateway:DeletePrivateNatGateway
189189
privatenat_dnat_rule_list = otcextensions.osclient.privatenat.v3.dnat:ListPrivateDnatRules
190+
privatenat_dnat_rule_create = otcextensions.osclient.privatenat.v3.dnat:CreatePrivateDnatRule
190191

191192
openstack.auto_scaling.v1 =
192193
as_group_list = otcextensions.osclient.auto_scaling.v1.group:ListAutoScalingGroup

0 commit comments

Comments
 (0)