From 816abfce7251a60d5bf60e8e486d0fb8f9c90910 Mon Sep 17 00:00:00 2001 From: Shushanik Hovhannesyan Date: Mon, 16 Feb 2026 20:29:25 +0100 Subject: [PATCH 1/6] Add new api for private_nat gateways. Only querying list --- otcextensions/osclient/nat/v3/__init__.py | 0 .../osclient/nat/v3/private_gateway.py | 149 ++++++++++++++++++ otcextensions/sdk/natv3/v3/_proxy.py | 11 ++ otcextensions/sdk/natv3/v3/gateway.py | 115 ++++++++++++++ .../tests/unit/osclient/nat/v3/fakes.py | 49 ++++++ .../osclient/nat/v3/test_private_gateway.py | 126 +++++++++++++++ .../tests/unit/sdk/natv3/__init__.py | 0 .../tests/unit/sdk/natv3/v3/__init__.py | 0 .../tests/unit/sdk/natv3/v3/test_gateway.py | 78 +++++++++ .../tests/unit/sdk/natv3/v3/test_proxy.py | 28 ++++ setup.cfg | 3 + 11 files changed, 559 insertions(+) create mode 100644 otcextensions/osclient/nat/v3/__init__.py create mode 100644 otcextensions/osclient/nat/v3/private_gateway.py create mode 100644 otcextensions/sdk/natv3/v3/gateway.py create mode 100644 otcextensions/tests/unit/osclient/nat/v3/fakes.py create mode 100644 otcextensions/tests/unit/osclient/nat/v3/test_private_gateway.py create mode 100644 otcextensions/tests/unit/sdk/natv3/__init__.py create mode 100644 otcextensions/tests/unit/sdk/natv3/v3/__init__.py create mode 100644 otcextensions/tests/unit/sdk/natv3/v3/test_gateway.py create mode 100644 otcextensions/tests/unit/sdk/natv3/v3/test_proxy.py diff --git a/otcextensions/osclient/nat/v3/__init__.py b/otcextensions/osclient/nat/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/osclient/nat/v3/private_gateway.py b/otcextensions/osclient/nat/v3/private_gateway.py new file mode 100644 index 000000000..e94d4af83 --- /dev/null +++ b/otcextensions/osclient/nat/v3/private_gateway.py @@ -0,0 +1,149 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +"""Private NAT Gateway v3 action implementations""" + +import logging + +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class ListPrivateNatGateways(command.Lister): + + _description = _("List Private NAT Gateways.") + columns = ( + 'Id', + 'Name', + 'Spec', + 'Status', + 'Project Id', + 'Enterprise Project Id', + ) + + def get_parser(self, prog_name): + parser = super(ListPrivateNatGateways, self).get_parser(prog_name) + + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_("Specifies the number of records displayed on each page."), + ) + parser.add_argument( + '--marker', + metavar='', + help=_("Specifies the start resource ID of pagination query."), + ) + parser.add_argument( + '--page-reverse', + action='store_true', + default=False, + help=_("Specifies whether to query resources" + " on the previous page."), + ) + parser.add_argument( + '--id', + metavar='', + nargs='+', + help=_("Specifies the private NAT gateway IDs."), + ) + parser.add_argument( + '--name', + metavar='', + nargs='+', + help=_("Specifies the private NAT gateway names."), + ) + parser.add_argument( + '--description', + metavar='', + nargs='+', + help=_("Provides supplementary information" + " about the private NAT gateway."), + ) + parser.add_argument( + '--spec', + metavar='', + nargs='+', + help=_("Specifies the private NAT gateway specifications. "), + ) + parser.add_argument( + '--project-id', + metavar='', + nargs='+', + help=_("Specifies the project ID. Repeat for multiple values."), + ) + parser.add_argument( + '--status', + metavar='', + nargs='+', + help=_("Specifies the private NAT gateway status."), + ) + parser.add_argument( + '--vpc-id', + metavar='', + nargs='+', + help=_("Specifies the ID of the VPC."), + ) + parser.add_argument( + '--virsubnet-id', + metavar='', + nargs='+', + help=_("Specifies the ID of the subnet."), + ) + parser.add_argument( + '--enterprise-project-id', + metavar='', + nargs='+', + help=_("Specifies the enterprise project ID " + "associated with the private NAT gateway."), + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.nat + + attrs = {} + args_list = [ + 'limit', + 'marker', + 'page_reverse', + 'id', + 'name', + 'description', + 'spec', + 'project_id', + 'status', + 'vpc_id', + 'virsubnet_id', + 'enterprise_project_id', + ] + for arg in args_list: + val = getattr(parsed_args, arg) + if val is not None and val != [] and val != '': + attrs[arg] = val + + data = client.private_nat_gateways(**attrs) + + return ( + self.columns, + ( + utils.get_item_properties(s, self.columns) + for s in data + ) + ) diff --git a/otcextensions/sdk/natv3/v3/_proxy.py b/otcextensions/sdk/natv3/v3/_proxy.py index 1b3ec9e89..feece5abc 100644 --- a/otcextensions/sdk/natv3/v3/_proxy.py +++ b/otcextensions/sdk/natv3/v3/_proxy.py @@ -12,6 +12,7 @@ from openstack import proxy from otcextensions.common.utils import extract_url_parts +from otcextensions.sdk.natv3.v3 import gateway as _gateway class Proxy(proxy.Proxy): @@ -20,3 +21,13 @@ class Proxy(proxy.Proxy): def _extract_name(self, url, service_type=None, project_id=None): return extract_url_parts(url, project_id) + + def private_nat_gateways(self, **query): + """Return a generator of private NAT gateways. + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of private NAT gateway objects. + """ + return self._list(_gateway.PrivateNatGateway, **query) diff --git a/otcextensions/sdk/natv3/v3/gateway.py b/otcextensions/sdk/natv3/v3/gateway.py new file mode 100644 index 000000000..cb5b9775d --- /dev/null +++ b/otcextensions/sdk/natv3/v3/gateway.py @@ -0,0 +1,115 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack import resource + + +class DownlinkVpc(resource.Resource): + #: Specifies the ID of the VPC where the private NAT gateway works. + vpc_id = resource.Body('vpc_id') + + #: Specifies the ID of the subnet where the private NAT gateway works. + virsubnet_id = resource.Body('virsubnet_id') + + #: Specifies the private IP address of the private NAT gateway. + ngport_ip_address = resource.Body('ngport_ip_address') + + + +class Tag(resource.Resource): + #: Specifies the tag key. Up to 128 Unicode characters. Cannot be blank. + key = resource.Body('key') + + #: Specifies the tag value. Up to 255 Unicode characters. + value = resource.Body('value') + + +class PrivateNatGateway(resource.Resource): + resources_key = 'gateways' + resource_key = 'gateway' + base_path = '/v3/%(project_id)s/private-nat/gateways' + + allow_list = True + + _query_mapping = resource.QueryParameters( + 'description', + 'enterprise_project_id', + 'id', + 'limit', + 'marker', + 'name', + 'page_reverse', + 'project_id', + 'spec', + 'status', + 'virsubnet_id', + 'vpc_id', + ) + + # Properties + #: Specifies the time when the private NAT gateway was created. + #: It is a UTC time in yyyy-mm-ddThh:mm:ssZ format. + created_at = resource.Body('created_at') + #: Provides supplementary information about the private NAT gateway. + #: The description can contain up to 255 characters. + #: Cannot contain angle brackets (<>). + description = resource.Body('description') + #: Specifies the VPC where the private NAT gateway works. + #: Each item contains: + #: * vpc_id: Specifies the ID of the VPC. + #: * virsubnet_id: Specifies the ID of the subnet. + #: * ngport_ip_address: Specifies the private IP address of the gateway. + downlink_vpcs = resource.Body('downlink_vpcs', type=list, list_type=DownlinkVpc) + #: Specifies the ID of the enterprise project that is associated + #: with the private NAT gateway when the private NAT gateway is created. + enterprise_project_id = resource.Body('enterprise_project_id') + #: Specifies the private NAT gateway ID. + id = resource.Body('id') + #: Specifies the private NAT gateway name. + name = resource.Body('name') + #: Specifies the project ID. + project_id = resource.Body('project_id') + #: Specifies the maximum number of rules. + #: Value range: 0–65535. + rule_max = resource.Body('rule_max', type=int) + #: Specifies the private NAT gateway specifications. + #: Enumeration values: + #: * Small (default) + #: * Medium + #: * Large + #: * Extra-large + spec = resource.Body('spec') + #: Specifies the private NAT gateway status. + #: Enumeration values: + #: * ACTIVE: The private NAT gateway is running properly. + #: * FROZEN: The private NAT gateway is frozen. + status = resource.Body('status') + #: Specifies the list of tags. + #: Each tag contains: + #: * key: Specifies the tag key. Up to 128 Unicode characters. + #: * value: Specifies the tag value. Up to 255 Unicode characters. + tags = resource.Body('tags', type=list, list_type=Tag) + #: Specifies the maximum number of transit IP addresses + #: in a transit IP address pool. + #: Value range: 1–100. + transit_ip_pool_size_max = resource.Body('transit_ip_pool_size_max', + type=int) + #: Specifies the time when the private NAT gateway was updated. + #: It is a UTC time in yyyy-mm-ddThh:mm:ssZ format. + updated_at = resource.Body('updated_at') + + + +class PageInfo(resource.Resource): + next_marker = resource.Body('next_marker') + previous_marker = resource.Body('previous_marker') + current_count = resource.Body('current_count', type=int) diff --git a/otcextensions/tests/unit/osclient/nat/v3/fakes.py b/otcextensions/tests/unit/osclient/nat/v3/fakes.py new file mode 100644 index 000000000..c62c71afc --- /dev/null +++ b/otcextensions/tests/unit/osclient/nat/v3/fakes.py @@ -0,0 +1,49 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import uuid + +import mock + +from openstackclient.tests.unit import utils + +from otcextensions.tests.unit.osclient import test_base + +from otcextensions.sdk.natv3.v3 import gateway + + +def gen_data(data, columns): + return tuple(getattr(data, attr, '') for attr in columns) + + +class TestNat(utils.TestCommand): + def setUp(self): + super(TestNat, self).setUp() + + self.app.client_manager.nat = mock.Mock() + + self.client = self.app.client_manager.nat + + +class FakePrivateNatGateway(test_base.Fake): + @classmethod + def generate(cls): + object_info = { + 'id': 'id-' + uuid.uuid4().hex, + 'name': 'name-' + uuid.uuid4().hex, + 'spec': 'Small', + 'status': 'ACTIVE', + 'project_id': 'project-' + uuid.uuid4().hex, + 'enterprise_project_id': 'ep-' + uuid.uuid4().hex, + } + + return gateway.PrivateNatGateway(**object_info) diff --git a/otcextensions/tests/unit/osclient/nat/v3/test_private_gateway.py b/otcextensions/tests/unit/osclient/nat/v3/test_private_gateway.py new file mode 100644 index 000000000..db7800c24 --- /dev/null +++ b/otcextensions/tests/unit/osclient/nat/v3/test_private_gateway.py @@ -0,0 +1,126 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import mock + +from otcextensions.osclient.nat.v3 import private_gateway +from otcextensions.tests.unit.osclient.nat.v3 import fakes + + +class TestListPrivateNatGateways(fakes.TestNat): + + objects = fakes.FakePrivateNatGateway.create_multiple(3) + + column_list_headers = ( + 'Id', + 'Name', + 'Spec', + 'Status', + 'Project Id', + 'Enterprise Project Id', + ) + + columns = ( + 'id', + 'name', + 'spec', + 'status', + 'project_id', + 'enterprise_project_id', + ) + + data = [] + + for s in objects: + data.append(( + s.id, + s.name, + s.spec, + s.status, + s.project_id, + s.enterprise_project_id, + )) + + def setUp(self): + super(TestListPrivateNatGateways, self).setUp() + + self.cmd = private_gateway.ListPrivateNatGateways(self.app, None) + + self.client.private_nat_gateways = mock.Mock() + self.client.api_mock = self.client.private_nat_gateways + + def test_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.client.api_mock.side_effect = [self.objects] + + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with() + + self.assertEqual(self.column_list_headers, columns) + self.assertEqual(self.data, list(data)) + + def test_list_args(self): + arglist = [ + '--limit', '1', + '--marker', 'm1', + '--page-reverse', + '--id', 'id1', 'id2', + '--name', 'n1', + '--description', 'd1', + '--spec', 'Small', + '--project-id', 'p1', + '--status', 'ACTIVE', + '--vpc-id', 'v1', + '--virsubnet-id', 's1', + '--enterprise-project-id', 'ep1', + ] + + verifylist = [ + ('limit', 1), + ('marker', 'm1'), + ('page_reverse', True), + ('id', ['id1', 'id2']), + ('name', ['n1']), + ('description', ['d1']), + ('spec', ['Small']), + ('project_id', ['p1']), + ('status', ['ACTIVE']), + ('vpc_id', ['v1']), + ('virsubnet_id', ['s1']), + ('enterprise_project_id', ['ep1']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.client.api_mock.side_effect = [self.objects] + + self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with( + limit=1, + marker='m1', + page_reverse=True, + id=['id1', 'id2'], + name=['n1'], + description=['d1'], + spec=['Small'], + project_id=['p1'], + status=['ACTIVE'], + vpc_id=['v1'], + virsubnet_id=['s1'], + enterprise_project_id=['ep1'], + ) diff --git a/otcextensions/tests/unit/sdk/natv3/__init__.py b/otcextensions/tests/unit/sdk/natv3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/unit/sdk/natv3/v3/__init__.py b/otcextensions/tests/unit/sdk/natv3/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/unit/sdk/natv3/v3/test_gateway.py b/otcextensions/tests/unit/sdk/natv3/v3/test_gateway.py new file mode 100644 index 000000000..39b08ce67 --- /dev/null +++ b/otcextensions/tests/unit/sdk/natv3/v3/test_gateway.py @@ -0,0 +1,78 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack.tests.unit import base + +from otcextensions.sdk.natv3.v3 import gateway + + +INSTANCE_ID = '14338426-6afe-4019-996b-3a9525296e11' +PROJECT_ID = '70505c941b9b4dfd82fd351932328a2f' +DOWNLINK_VPC = { + 'vpc_id': '3cb66d44-9f75-4237-bfff-e37b14d23ad2', + 'virsubnet_id': '373979ee-f4f0-46c5-80e3-0fbf72646b70', + 'ngport_ip_address': '10.0.0.17' +} +TAGS = {'key': 'key1', 'value': 'value1'} +EXAMPLE = { + 'id': INSTANCE_ID, + 'name': 'private-nat-gateway-name1', + 'description': 'private-nat-gateway-description1', + 'spec': 'Small', + 'project_id': PROJECT_ID, + 'enterprise_project_id': '2759da7b-8015-404c-ae0a-a389007b0e2a', + 'status': 'ACTIVE', + 'created_at': '2019-04-22T08:47:13', + 'updated_at': '2019-04-22T08:47:13', + 'tags': [TAGS], + 'downlink_vpcs': [DOWNLINK_VPC], + 'transit_ip_pool_size_max': 1, + 'rule_max': 20 +} + + +class TestPrivateNatGateway(base.TestCase): + + def test_basic(self): + sot = gateway.PrivateNatGateway() + self.assertEqual('gateways', sot.resources_key) + self.assertEqual('gateway', sot.resource_key) + self.assertEqual('/v3/%(project_id)s/private-nat/gateways', + sot.base_path) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = gateway.PrivateNatGateway(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['spec'], sot.spec) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(1, len(sot.downlink_vpcs)) + self.assertIsInstance(sot.downlink_vpcs[0], gateway.DownlinkVpc) + self.assertEqual(DOWNLINK_VPC['vpc_id'], sot.downlink_vpcs[0].vpc_id) + self.assertEqual(DOWNLINK_VPC['virsubnet_id'], + sot.downlink_vpcs[0].virsubnet_id) + self.assertEqual(DOWNLINK_VPC['ngport_ip_address'], + sot.downlink_vpcs[0].ngport_ip_address) + self.assertEqual(1, len(sot.tags)) + self.assertIsInstance(sot.tags[0], gateway.Tag) + self.assertEqual(TAGS['key'], sot.tags[0].key) + self.assertEqual(TAGS['value'], sot.tags[0].value) + self.assertEqual(EXAMPLE['enterprise_project_id'], + sot.enterprise_project_id) + self.assertEqual(EXAMPLE['rule_max'], sot.rule_max) + self.assertEqual(EXAMPLE['transit_ip_pool_size_max'], + sot.transit_ip_pool_size_max) diff --git a/otcextensions/tests/unit/sdk/natv3/v3/test_proxy.py b/otcextensions/tests/unit/sdk/natv3/v3/test_proxy.py new file mode 100644 index 000000000..efc49e49b --- /dev/null +++ b/otcextensions/tests/unit/sdk/natv3/v3/test_proxy.py @@ -0,0 +1,28 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from otcextensions.sdk.natv3.v3 import _proxy +from otcextensions.sdk.natv3.v3 import gateway + +from openstack.tests.unit import test_proxy_base + + +class TestNatv3Proxy(test_proxy_base.TestProxyBase): + def setUp(self): + super(TestNatv3Proxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + +class TestPrivateNatGateway(TestNatv3Proxy): + def test_private_nat_gateways(self): + self.verify_list(self.proxy.private_nat_gateways, + gateway.PrivateNatGateway) diff --git a/setup.cfg b/setup.cfg index 07e96bc35..23dcb0a91 100644 --- a/setup.cfg +++ b/setup.cfg @@ -179,6 +179,9 @@ openstack.nat.v2 = nat_dnat_rule_create = otcextensions.osclient.nat.v2.dnat:CreateDnatRule nat_dnat_rule_delete = otcextensions.osclient.nat.v2.dnat:DeleteDnatRule +openstack.nat.v3 = + nat_private_gateway_list = otcextensions.osclient.nat.v3.private_gateway:ListPrivateNatGateways + openstack.auto_scaling.v1 = as_group_list = otcextensions.osclient.auto_scaling.v1.group:ListAutoScalingGroup as_group_show = otcextensions.osclient.auto_scaling.v1.group:ShowAutoScalingGroup From a0c311809f61c9a90c4d02c6d1b213fc2be99ac0 Mon Sep 17 00:00:00 2001 From: Shushanik Hovhannesyan Date: Wed, 18 Feb 2026 16:11:43 +0100 Subject: [PATCH 2/6] Add documentation and fix lint errors --- doc/source/cli/index.rst | 1 + doc/source/cli/privatenat.rst | 19 +++++++++++++++ doc/source/sdk/proxies/index.rst | 1 + doc/source/sdk/proxies/privatenat.rst | 19 +++++++++++++++ doc/source/sdk/resources/index.rst | 1 + doc/source/sdk/resources/privatenat/index.rst | 7 ++++++ .../privatenat/v3/private_gateway.rst | 13 +++++++++++ examples/natv3/list_private_gateways.py | 23 +++++++++++++++++++ otcextensions/sdk/natv3/v3/gateway.py | 5 ++-- 9 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 doc/source/cli/privatenat.rst create mode 100644 doc/source/sdk/proxies/privatenat.rst create mode 100644 doc/source/sdk/resources/privatenat/index.rst create mode 100644 doc/source/sdk/resources/privatenat/v3/private_gateway.rst create mode 100644 examples/natv3/list_private_gateways.py diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 942a01be4..1eb827519 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -42,6 +42,7 @@ documentation of these services: load_balancer modelarts nat + natv3 obs rds_v3 sdrs diff --git a/doc/source/cli/privatenat.rst b/doc/source/cli/privatenat.rst new file mode 100644 index 000000000..ce95e9346 --- /dev/null +++ b/doc/source/cli/privatenat.rst @@ -0,0 +1,19 @@ +Private Network Address Translation (Private NAT) +================================================= + +The Private NAT client is the command-line interface (CLI) for +the Private NAT API. + +For help on a specific `natv3` command, enter: + +.. code-block:: console + + $ openstack natv3 help SUBCOMMAND + +.. _private_gateway: + +Private NAT Gateway Operations +------------------------------ + +.. autoprogram-cliff:: openstack.nat.v3 + :command: natv3 private_gateway * diff --git a/doc/source/sdk/proxies/index.rst b/doc/source/sdk/proxies/index.rst index 459e7310e..1cd5bfeae 100644 --- a/doc/source/sdk/proxies/index.rst +++ b/doc/source/sdk/proxies/index.rst @@ -29,6 +29,7 @@ Service Proxies MapReduce Service (MRS) Modelarts Service (ModelArts) Network Address Translation (NAT) + Private Network Address Translation (Private NAT) Object Block Storage (OBS) Relational Database Service RDS V1 (RDSv1) Relational Database Service RDS V3 (RDS) diff --git a/doc/source/sdk/proxies/privatenat.rst b/doc/source/sdk/proxies/privatenat.rst new file mode 100644 index 000000000..47f368514 --- /dev/null +++ b/doc/source/sdk/proxies/privatenat.rst @@ -0,0 +1,19 @@ +Private NAT API +=============== + +.. automodule:: otcextensions.sdk.natv3.v3._proxy + +The Private NAT high-level interface +------------------------------------ + +The private NAT high-level interface is available through the ``natv3`` +member of a :class:`~openstack.connection.Connection` object. The +``natv3`` member will only be added if the +``otcextensions.sdk.register_otc_extensions(conn)`` method is called. + +Private NAT Gateway Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.natv3.v3._proxy.Proxy + :noindex: + :members: private_nat_gateways diff --git a/doc/source/sdk/resources/index.rst b/doc/source/sdk/resources/index.rst index fd763dfdd..3c52e163a 100644 --- a/doc/source/sdk/resources/index.rst +++ b/doc/source/sdk/resources/index.rst @@ -30,6 +30,7 @@ Open Telekom Cloud Resources MapReduce Service (MRS) ModelArts Service (MA) Network Address Translation (NAT) + Private Network Address Translation (Private NAT) Object Block Storage (OBS) Relational Database Service (RDS) Shared File System Turbo (SFS Turbo) diff --git a/doc/source/sdk/resources/privatenat/index.rst b/doc/source/sdk/resources/privatenat/index.rst new file mode 100644 index 000000000..a8d43428a --- /dev/null +++ b/doc/source/sdk/resources/privatenat/index.rst @@ -0,0 +1,7 @@ +Private NAT Resources +===================== + +.. toctree:: + :maxdepth: 1 + + v3/private_gateway diff --git a/doc/source/sdk/resources/privatenat/v3/private_gateway.rst b/doc/source/sdk/resources/privatenat/v3/private_gateway.rst new file mode 100644 index 000000000..0e9d3f0a0 --- /dev/null +++ b/doc/source/sdk/resources/privatenat/v3/private_gateway.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.natv3.v3.gateway +=============================== + +.. automodule:: otcextensions.sdk.natv3.v3.gateway + +The Private NAT Gateway Class +----------------------------- + +The ``PrivateNatGateway`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: otcextensions.sdk.natv3.v3.gateway.PrivateNatGateway + :members: diff --git a/examples/natv3/list_private_gateways.py b/examples/natv3/list_private_gateways.py new file mode 100644 index 000000000..6c4ef5548 --- /dev/null +++ b/examples/natv3/list_private_gateways.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +List all Private NAT Gateways +""" +import openstack + + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +for gateway in conn.natv3.private_nat_gateways(): + print(gateway) diff --git a/otcextensions/sdk/natv3/v3/gateway.py b/otcextensions/sdk/natv3/v3/gateway.py index cb5b9775d..5fecd9228 100644 --- a/otcextensions/sdk/natv3/v3/gateway.py +++ b/otcextensions/sdk/natv3/v3/gateway.py @@ -24,7 +24,6 @@ class DownlinkVpc(resource.Resource): ngport_ip_address = resource.Body('ngport_ip_address') - class Tag(resource.Resource): #: Specifies the tag key. Up to 128 Unicode characters. Cannot be blank. key = resource.Body('key') @@ -68,7 +67,8 @@ class PrivateNatGateway(resource.Resource): #: * vpc_id: Specifies the ID of the VPC. #: * virsubnet_id: Specifies the ID of the subnet. #: * ngport_ip_address: Specifies the private IP address of the gateway. - downlink_vpcs = resource.Body('downlink_vpcs', type=list, list_type=DownlinkVpc) + downlink_vpcs = resource.Body('downlink_vpcs', type=list, + list_type=DownlinkVpc) #: Specifies the ID of the enterprise project that is associated #: with the private NAT gateway when the private NAT gateway is created. enterprise_project_id = resource.Body('enterprise_project_id') @@ -108,7 +108,6 @@ class PrivateNatGateway(resource.Resource): updated_at = resource.Body('updated_at') - class PageInfo(resource.Resource): next_marker = resource.Body('next_marker') previous_marker = resource.Body('previous_marker') From 1f6dec5c058ef78e72039072af38f5c4ee4d0eeb Mon Sep 17 00:00:00 2001 From: Shushanik Hovhannesyan Date: Thu, 19 Feb 2026 10:48:29 +0100 Subject: [PATCH 3/6] Fix tox docs errors --- doc/source/cli/index.rst | 1 + doc/source/sdk/resources/privatenat/v3/private_gateway.rst | 2 +- otcextensions/sdk/natv3/v3/gateway.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 1eb827519..86b8c9178 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -44,6 +44,7 @@ documentation of these services: nat natv3 obs + privatenat rds_v3 sdrs smn diff --git a/doc/source/sdk/resources/privatenat/v3/private_gateway.rst b/doc/source/sdk/resources/privatenat/v3/private_gateway.rst index 0e9d3f0a0..062e88f0e 100644 --- a/doc/source/sdk/resources/privatenat/v3/private_gateway.rst +++ b/doc/source/sdk/resources/privatenat/v3/private_gateway.rst @@ -1,5 +1,5 @@ otcextensions.sdk.natv3.v3.gateway -=============================== +================================== .. automodule:: otcextensions.sdk.natv3.v3.gateway diff --git a/otcextensions/sdk/natv3/v3/gateway.py b/otcextensions/sdk/natv3/v3/gateway.py index 5fecd9228..ce050ea20 100644 --- a/otcextensions/sdk/natv3/v3/gateway.py +++ b/otcextensions/sdk/natv3/v3/gateway.py @@ -60,7 +60,7 @@ class PrivateNatGateway(resource.Resource): created_at = resource.Body('created_at') #: Provides supplementary information about the private NAT gateway. #: The description can contain up to 255 characters. - #: Cannot contain angle brackets (<>). + #: Cannot contain angle brackets (<>). description = resource.Body('description') #: Specifies the VPC where the private NAT gateway works. #: Each item contains: From 64e1babdfc37ecaed0e0bf825d368aff6223c132 Mon Sep 17 00:00:00 2001 From: Shushanik Hovhannesyan Date: Thu, 26 Feb 2026 18:33:07 +0400 Subject: [PATCH 4/6] Add tests, fix doc issues, apply review comments --- doc/source/cli/index.rst | 1 - doc/source/cli/privatenat.rst | 8 +- .../{nat/v3 => privatenat}/__init__.py | 0 otcextensions/osclient/privatenat/client.py | 52 ++++++ .../osclient/privatenat/v3/__init__.py | 0 .../v3/private_nat_gateway.py} | 8 +- otcextensions/sdk/__init__.py | 1 + otcextensions/sdk/natv3/v3/gateway.py | 2 +- otcextensions/sdk/obs/v1/_proxy.py | 20 ++- .../osclient/privatenat/__init__.py | 0 .../osclient/privatenat/v3/__init__.py | 0 .../osclient/privatenat/v3/common.py | 166 ++++++++++++++++++ .../osclient/privatenat/v3/test_gateway.py | 28 +++ .../tests/functional/sdk/natv3/__init__.py | 0 .../tests/functional/sdk/natv3/v3/__init__.py | 0 .../functional/sdk/natv3/v3/test_gateway.py | 33 ++++ .../unit/osclient/privatenat/__init__.py | 0 .../osclient/{nat => privatenat}/v3/fakes.py | 11 +- .../v3/test_private_gateway.py | 8 +- setup.cfg | 5 +- tox.ini | 2 +- 21 files changed, 316 insertions(+), 29 deletions(-) rename otcextensions/osclient/{nat/v3 => privatenat}/__init__.py (100%) create mode 100644 otcextensions/osclient/privatenat/client.py create mode 100644 otcextensions/osclient/privatenat/v3/__init__.py rename otcextensions/osclient/{nat/v3/private_gateway.py => privatenat/v3/private_nat_gateway.py} (93%) create mode 100644 otcextensions/tests/functional/osclient/privatenat/__init__.py create mode 100644 otcextensions/tests/functional/osclient/privatenat/v3/__init__.py create mode 100644 otcextensions/tests/functional/osclient/privatenat/v3/common.py create mode 100644 otcextensions/tests/functional/osclient/privatenat/v3/test_gateway.py create mode 100644 otcextensions/tests/functional/sdk/natv3/__init__.py create mode 100644 otcextensions/tests/functional/sdk/natv3/v3/__init__.py create mode 100644 otcextensions/tests/functional/sdk/natv3/v3/test_gateway.py create mode 100644 otcextensions/tests/unit/osclient/privatenat/__init__.py rename otcextensions/tests/unit/osclient/{nat => privatenat}/v3/fakes.py (86%) rename otcextensions/tests/unit/osclient/{nat => privatenat}/v3/test_private_gateway.py (92%) diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 86b8c9178..751a800cd 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -42,7 +42,6 @@ documentation of these services: load_balancer modelarts nat - natv3 obs privatenat rds_v3 diff --git a/doc/source/cli/privatenat.rst b/doc/source/cli/privatenat.rst index ce95e9346..f140cd65d 100644 --- a/doc/source/cli/privatenat.rst +++ b/doc/source/cli/privatenat.rst @@ -4,16 +4,16 @@ Private Network Address Translation (Private NAT) The Private NAT client is the command-line interface (CLI) for the Private NAT API. -For help on a specific `natv3` command, enter: +For help on a specific `privatenat` command, enter: .. code-block:: console - $ openstack natv3 help SUBCOMMAND + $ openstack privatenat help SUBCOMMAND .. _private_gateway: Private NAT Gateway Operations ------------------------------ -.. autoprogram-cliff:: openstack.nat.v3 - :command: natv3 private_gateway * +.. autoprogram-cliff:: openstack.privatenat.v3 + :command: privatenat gateway * diff --git a/otcextensions/osclient/nat/v3/__init__.py b/otcextensions/osclient/privatenat/__init__.py similarity index 100% rename from otcextensions/osclient/nat/v3/__init__.py rename to otcextensions/osclient/privatenat/__init__.py diff --git a/otcextensions/osclient/privatenat/client.py b/otcextensions/osclient/privatenat/client.py new file mode 100644 index 000000000..aa93d7438 --- /dev/null +++ b/otcextensions/osclient/privatenat/client.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import logging + +from osc_lib import utils + +from otcextensions import sdk +from otcextensions.i18n import _ + +LOG = logging.getLogger(__name__) + +DEFAULT_API_VERSION = '3' +API_VERSION_OPTION = 'os_privatenat_api_version' +API_NAME = "privatenat" +API_VERSIONS = { + "3": "openstack.connection.Connection" +} + + +def make_client(instance): + """Returns a private NAT proxy""" + conn = instance.sdk_connection + + if getattr(conn, 'natv3', None) is None: + LOG.debug('OTC extensions are not registered. Do that now') + sdk.register_otc_extensions(conn) + + LOG.debug('Private NAT client initialized using OpenStack OTC SDK: %s', + conn.natv3) + return conn.natv3 + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-privatenat-api-version', + metavar='', + default=utils.env('OS_PRIVATENAT_API_VERSION'), + help=_("Private NAT API version, default=%s " + "(Env: OS_PRIVATENAT_API_VERSION)") % DEFAULT_API_VERSION + ) + return parser diff --git a/otcextensions/osclient/privatenat/v3/__init__.py b/otcextensions/osclient/privatenat/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/osclient/nat/v3/private_gateway.py b/otcextensions/osclient/privatenat/v3/private_nat_gateway.py similarity index 93% rename from otcextensions/osclient/nat/v3/private_gateway.py rename to otcextensions/osclient/privatenat/v3/private_nat_gateway.py index e94d4af83..fb4fc8866 100644 --- a/otcextensions/osclient/nat/v3/private_gateway.py +++ b/otcextensions/osclient/privatenat/v3/private_nat_gateway.py @@ -116,7 +116,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - client = self.app.client_manager.nat + client = self.app.client_manager.privatenat attrs = {} args_list = [ @@ -135,7 +135,11 @@ def take_action(self, parsed_args): ] for arg in args_list: val = getattr(parsed_args, arg) - if val is not None and val != [] and val != '': + if arg == 'page_reverse': + # Only send flag if explicitly requested + if val: + attrs[arg] = val + elif val is not None and val != [] and val != '': attrs[arg] = val data = client.private_nat_gateways(**attrs) diff --git a/otcextensions/sdk/__init__.py b/otcextensions/sdk/__init__.py index 17cb6c155..9c9fb89c2 100644 --- a/otcextensions/sdk/__init__.py +++ b/otcextensions/sdk/__init__.py @@ -269,6 +269,7 @@ }, 'natv3': { 'service_type': 'natv3', + 'append_project_id': True, 'endpoint_service_type': 'natv3' }, 'obs': { diff --git a/otcextensions/sdk/natv3/v3/gateway.py b/otcextensions/sdk/natv3/v3/gateway.py index ce050ea20..459f3adb1 100644 --- a/otcextensions/sdk/natv3/v3/gateway.py +++ b/otcextensions/sdk/natv3/v3/gateway.py @@ -35,7 +35,7 @@ class Tag(resource.Resource): class PrivateNatGateway(resource.Resource): resources_key = 'gateways' resource_key = 'gateway' - base_path = '/v3/%(project_id)s/private-nat/gateways' + base_path = '/private-nat/gateways' allow_list = True diff --git a/otcextensions/sdk/obs/v1/_proxy.py b/otcextensions/sdk/obs/v1/_proxy.py index 5702db391..018f88c54 100644 --- a/otcextensions/sdk/obs/v1/_proxy.py +++ b/otcextensions/sdk/obs/v1/_proxy.py @@ -794,10 +794,12 @@ def wait_for_delete_object(self, obj, container=None, obj = self._get_resource(_obj.Object, obj) container = self._get_container_name(obj, container) for _ in os_utils.iterate_timeout( - timeout=wait, - message=f"Timeout waiting for {obj.name} " - f"in {container} to delete", - wait=interval, + timeout=wait, + message=( + f"Timeout waiting for {obj.name} " + f"in {container} to delete" + ), + wait=interval, ): try: obj.fetch(self, container=container, skip_cache=True) @@ -825,10 +827,12 @@ def wait_for_delete_container(self, container, interval=2, wait=180): container = self._get_resource(_container.Container, container) for _ in os_utils.iterate_timeout( - timeout=wait, - message=f"Timeout waiting for container" - f" {container.name} to delete", - wait=interval, + timeout=wait, + message=( + f"Timeout waiting for container" + f" {container.name} to delete" + ), + wait=interval, ): try: self.get_container(container.name) diff --git a/otcextensions/tests/functional/osclient/privatenat/__init__.py b/otcextensions/tests/functional/osclient/privatenat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/functional/osclient/privatenat/v3/__init__.py b/otcextensions/tests/functional/osclient/privatenat/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/functional/osclient/privatenat/v3/common.py b/otcextensions/tests/functional/osclient/privatenat/v3/common.py new file mode 100644 index 000000000..ea3e77510 --- /dev/null +++ b/otcextensions/tests/functional/osclient/privatenat/v3/common.py @@ -0,0 +1,166 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import json +import uuid + +from datetime import datetime + +from openstackclient.tests.functional import base + + +class NatTestCase(base.TestCase): + """Common functional test bits for NAT commands""" + + CURR_TIME = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + + def setUp(self): + super(NatTestCase, self).setUp() + UUID = uuid.uuid4().hex[:8] + self.ROUTER_NAME = 'otce-nat-test-router-' + UUID + self.NETWORK_NAME = 'otce-nat-test-net-' + UUID + self.SUBNET_NAME = 'otce-nat-test-subnet-' + UUID + self.NAT_NAME = 'otce-nat-test-' + UUID + + self.ROUTER_ID = None + self.NETWORK_ID = None + self.FLOATING_IP_ID = None + self.NAT_ID = None + self.SNAT_RULE_ID = None + self.DNAT_RULE_ID = None + + def create_nat_gateway(self, name=None): + self._initialize_network() + name = name or self.NAT_NAME + json_output = json.loads(self.openstack( + 'nat gateway create {name}' + ' --router-id {router_id}' + ' --internal-network-id {net_id}' + ' --description {description}' + ' --spec {spec} -f json'.format( + name=name, + router_id=self.ROUTER_ID, + net_id=self.NETWORK_ID, + description='OTCE_func_test', + spec=1) + )) + self.assertIsNotNone(json_output) + self.NAT_ID = json_output['id'] + return json_output + + def delete_nat_gateway(self): + self.addCleanup(self._denitialize_network) + self.openstack('nat gateway delete ' + self.NAT_ID) + + def create_snat_rule(self): + nat_gateway = self.create_nat_gateway() + self.assertIsNotNone(nat_gateway) + self.assertIsNotNone(self.FLOATING_IP_ID) + json_output = json.loads(self.openstack( + 'nat snat rule create ' + '--nat-gateway-id {nat_gateway_id} ' + '--floating-ip-id {floating_ip_id} ' + '--network-id {net_id} -f json'.format( + nat_gateway_id=nat_gateway['id'], + floating_ip_id=self.FLOATING_IP_ID, + net_id=self.NETWORK_ID) + )) + self.assertIsNotNone(json_output) + self.SNAT_RULE_ID = json_output['id'] + return json_output + + def delete_snat_rule(self): + self.addCleanup(self.delete_nat_gateway) + self.openstack( + 'nat snat rule delete ' + self.SNAT_RULE_ID) + + def create_dnat_rule(self): + nat_gateway = self.create_nat_gateway() + self.assertIsNotNone(nat_gateway) + self.assertIsNotNone(self.FLOATING_IP_ID) + json_output = json.loads(self.openstack( + 'nat dnat rule create ' + '--nat-gateway-id {nat_gateway_id} ' + '--floating-ip-id {floating_ip_id} ' + '--protocol {protocol} ' + '--internal-service-port 80 ' + '--external-service-port 80 ' + '--private-ip {private_ip} ' + '-f json'.format( + nat_gateway_id=nat_gateway['id'], + protocol='TCP', + private_ip='192.168.0.3', + floating_ip_id=self.FLOATING_IP_ID) + )) + self.assertIsNotNone(json_output) + self.DNAT_RULE_ID = json_output['id'] + return json_output + + def delete_dnat_rule(self): + self.addCleanup(self.delete_nat_gateway) + self.openstack( + 'nat dnat rule delete ' + self.DNAT_RULE_ID) + + def _initialize_network(self): + router = json.loads(self.openstack( + 'router create -f json ' + self.ROUTER_NAME + )) + net = json.loads(self.openstack( + 'network create -f json ' + self.NETWORK_NAME + )) + self.openstack( + 'subnet create {subnet} -f json ' + '--network {net} ' + '--subnet-range 192.168.0.0/24 '.format( + subnet=self.SUBNET_NAME, + net=self.NETWORK_NAME + )) + + self.openstack( + 'router add subnet {router} ' + '{subnet} '.format( + router=self.ROUTER_NAME, + subnet=self.SUBNET_NAME + ) + ) + + floating_ip = json.loads(self.openstack( + 'floating ip create -f json ' + '{network}'.format( + network='admin_external_net') + )) + + self.ROUTER_ID = router['id'] + self.NETWORK_ID = net['id'] + self.FLOATING_IP_ID = floating_ip['id'] + + def _denitialize_network(self): + self.openstack( + 'floating ip delete ' + self.FLOATING_IP_ID + ) + self.openstack( + 'router remove subnet {router} ' + '{subnet} '.format( + router=self.ROUTER_NAME, + subnet=self.SUBNET_NAME + ) + ) + self.openstack( + 'subnet delete ' + self.SUBNET_NAME + ) + self.openstack( + 'network delete ' + self.NETWORK_NAME + ) + self.openstack( + 'router delete ' + self.ROUTER_NAME + ) diff --git a/otcextensions/tests/functional/osclient/privatenat/v3/test_gateway.py b/otcextensions/tests/functional/osclient/privatenat/v3/test_gateway.py new file mode 100644 index 000000000..bb0e70bc1 --- /dev/null +++ b/otcextensions/tests/functional/osclient/privatenat/v3/test_gateway.py @@ -0,0 +1,28 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from otcextensions.tests.functional.osclient.privatenat.v3 import common + + +class TestGateway(common.PrivateNatTestCase): + """Functional Tests for Private NAT Gateway""" + + def setUp(self): + super(TestGateway, self).setUp() + + def test_private_nat_gateway_list(self): + json_output = json.loads(self.openstack( + 'privatenat gateway list -f json ' + )) + self.assertIsNotNone(json_output) diff --git a/otcextensions/tests/functional/sdk/natv3/__init__.py b/otcextensions/tests/functional/sdk/natv3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/functional/sdk/natv3/v3/__init__.py b/otcextensions/tests/functional/sdk/natv3/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/functional/sdk/natv3/v3/test_gateway.py b/otcextensions/tests/functional/sdk/natv3/v3/test_gateway.py new file mode 100644 index 000000000..071ce3077 --- /dev/null +++ b/otcextensions/tests/functional/sdk/natv3/v3/test_gateway.py @@ -0,0 +1,33 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import openstack +import uuid + +from otcextensions.tests.functional import base + +_logger = openstack._log.setup_logging('openstack') + + +class TestGateway(base.BaseFunctionalTest): + uuid_v4 = uuid.uuid4().hex[:8] + update_gateway_name = uuid_v4 + 'update-test-gateway' + network_info = None + gateway = None + gateway_name = uuid_v4 + 'test-gateway-gateway' + attrs = { + "name": gateway_name, + "spec": "1" + } + + def test_01_list_gateways(self): + self.gateways = list(self.conn.natv3.private_nat_gateways()) + self.assertGreaterEqual(len(self.gateways), 0) diff --git a/otcextensions/tests/unit/osclient/privatenat/__init__.py b/otcextensions/tests/unit/osclient/privatenat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/unit/osclient/nat/v3/fakes.py b/otcextensions/tests/unit/osclient/privatenat/v3/fakes.py similarity index 86% rename from otcextensions/tests/unit/osclient/nat/v3/fakes.py rename to otcextensions/tests/unit/osclient/privatenat/v3/fakes.py index c62c71afc..995cd8150 100644 --- a/otcextensions/tests/unit/osclient/nat/v3/fakes.py +++ b/otcextensions/tests/unit/osclient/privatenat/v3/fakes.py @@ -16,22 +16,21 @@ from openstackclient.tests.unit import utils -from otcextensions.tests.unit.osclient import test_base - from otcextensions.sdk.natv3.v3 import gateway +from otcextensions.tests.unit.osclient import test_base def gen_data(data, columns): return tuple(getattr(data, attr, '') for attr in columns) -class TestNat(utils.TestCommand): +class TestPrivateNat(utils.TestCommand): def setUp(self): - super(TestNat, self).setUp() + super(TestPrivateNat, self).setUp() - self.app.client_manager.nat = mock.Mock() + self.app.client_manager.privatenat = mock.Mock() - self.client = self.app.client_manager.nat + self.client = self.app.client_manager.privatenat class FakePrivateNatGateway(test_base.Fake): diff --git a/otcextensions/tests/unit/osclient/nat/v3/test_private_gateway.py b/otcextensions/tests/unit/osclient/privatenat/v3/test_private_gateway.py similarity index 92% rename from otcextensions/tests/unit/osclient/nat/v3/test_private_gateway.py rename to otcextensions/tests/unit/osclient/privatenat/v3/test_private_gateway.py index db7800c24..5470afed2 100644 --- a/otcextensions/tests/unit/osclient/nat/v3/test_private_gateway.py +++ b/otcextensions/tests/unit/osclient/privatenat/v3/test_private_gateway.py @@ -12,11 +12,11 @@ # import mock -from otcextensions.osclient.nat.v3 import private_gateway -from otcextensions.tests.unit.osclient.nat.v3 import fakes +from otcextensions.osclient.privatenat.v3 import private_nat_gateway +from otcextensions.tests.unit.osclient.privatenat.v3 import fakes -class TestListPrivateNatGateways(fakes.TestNat): +class TestListPrivateNatGateways(fakes.TestPrivateNat): objects = fakes.FakePrivateNatGateway.create_multiple(3) @@ -53,7 +53,7 @@ class TestListPrivateNatGateways(fakes.TestNat): def setUp(self): super(TestListPrivateNatGateways, self).setUp() - self.cmd = private_gateway.ListPrivateNatGateways(self.app, None) + self.cmd = private_nat_gateway.ListPrivateNatGateways(self.app, None) self.client.private_nat_gateways = mock.Mock() self.client.api_mock = self.client.private_nat_gateways diff --git a/setup.cfg b/setup.cfg index 23dcb0a91..ccac91f6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ openstack.cli.extension = deh = otcextensions.osclient.deh.client mrs = otcextensions.osclient.mrs.client nat = otcextensions.osclient.nat.client + privatenat = otcextensions.osclient.privatenat.client vpc = otcextensions.osclient.vpc.client vpcep = otcextensions.osclient.vpcep.client dlb = otcextensions.osclient.vlb.client @@ -179,8 +180,8 @@ openstack.nat.v2 = nat_dnat_rule_create = otcextensions.osclient.nat.v2.dnat:CreateDnatRule nat_dnat_rule_delete = otcextensions.osclient.nat.v2.dnat:DeleteDnatRule -openstack.nat.v3 = - nat_private_gateway_list = otcextensions.osclient.nat.v3.private_gateway:ListPrivateNatGateways +openstack.privatenat.v3 = + privatenat_gateway_list = otcextensions.osclient.privatenat.v3.private_nat_gateway:ListPrivateNatGateways openstack.auto_scaling.v1 = as_group_list = otcextensions.osclient.auto_scaling.v1.group:ListAutoScalingGroup diff --git a/tox.ini b/tox.ini index 67bc0535a..0ecc08c57 100644 --- a/tox.ini +++ b/tox.ini @@ -87,7 +87,7 @@ commands = # W503 Is supposed to be off by default but in the latest pycodestyle isn't. # Also, both openstacksdk and Donald Knuth disagree with the rule. Line # breaks should occur before the binary operator for readability. -ignore = H306,H4,W503,E231 +ignore = H306,H4,W503,E231,E713 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,openstack/_services_mixin.py From ebf36b5c03134d9015b7045906a279214046f6c1 Mon Sep 17 00:00:00 2001 From: Shushanik Hovhannesyan Date: Thu, 26 Feb 2026 18:37:56 +0400 Subject: [PATCH 5/6] Remove unused data from test --- .../tests/functional/sdk/natv3/v3/test_gateway.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/otcextensions/tests/functional/sdk/natv3/v3/test_gateway.py b/otcextensions/tests/functional/sdk/natv3/v3/test_gateway.py index 071ce3077..b417a63f1 100644 --- a/otcextensions/tests/functional/sdk/natv3/v3/test_gateway.py +++ b/otcextensions/tests/functional/sdk/natv3/v3/test_gateway.py @@ -18,15 +18,6 @@ class TestGateway(base.BaseFunctionalTest): - uuid_v4 = uuid.uuid4().hex[:8] - update_gateway_name = uuid_v4 + 'update-test-gateway' - network_info = None - gateway = None - gateway_name = uuid_v4 + 'test-gateway-gateway' - attrs = { - "name": gateway_name, - "spec": "1" - } def test_01_list_gateways(self): self.gateways = list(self.conn.natv3.private_nat_gateways()) From 18755f2060445ba747c1d6100cefff3894d85b32 Mon Sep 17 00:00:00 2001 From: Shushanik Hovhannesyan Date: Thu, 26 Feb 2026 19:01:10 +0400 Subject: [PATCH 6/6] Fix test issue --- .../osclient/privatenat/v3/common.py | 166 ------------------ ...gateway.py => test_private_nat_gateway.py} | 6 +- 2 files changed, 3 insertions(+), 169 deletions(-) delete mode 100644 otcextensions/tests/functional/osclient/privatenat/v3/common.py rename otcextensions/tests/functional/osclient/privatenat/v3/{test_gateway.py => test_private_nat_gateway.py} (84%) diff --git a/otcextensions/tests/functional/osclient/privatenat/v3/common.py b/otcextensions/tests/functional/osclient/privatenat/v3/common.py deleted file mode 100644 index ea3e77510..000000000 --- a/otcextensions/tests/functional/osclient/privatenat/v3/common.py +++ /dev/null @@ -1,166 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -import json -import uuid - -from datetime import datetime - -from openstackclient.tests.functional import base - - -class NatTestCase(base.TestCase): - """Common functional test bits for NAT commands""" - - CURR_TIME = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") - - def setUp(self): - super(NatTestCase, self).setUp() - UUID = uuid.uuid4().hex[:8] - self.ROUTER_NAME = 'otce-nat-test-router-' + UUID - self.NETWORK_NAME = 'otce-nat-test-net-' + UUID - self.SUBNET_NAME = 'otce-nat-test-subnet-' + UUID - self.NAT_NAME = 'otce-nat-test-' + UUID - - self.ROUTER_ID = None - self.NETWORK_ID = None - self.FLOATING_IP_ID = None - self.NAT_ID = None - self.SNAT_RULE_ID = None - self.DNAT_RULE_ID = None - - def create_nat_gateway(self, name=None): - self._initialize_network() - name = name or self.NAT_NAME - json_output = json.loads(self.openstack( - 'nat gateway create {name}' - ' --router-id {router_id}' - ' --internal-network-id {net_id}' - ' --description {description}' - ' --spec {spec} -f json'.format( - name=name, - router_id=self.ROUTER_ID, - net_id=self.NETWORK_ID, - description='OTCE_func_test', - spec=1) - )) - self.assertIsNotNone(json_output) - self.NAT_ID = json_output['id'] - return json_output - - def delete_nat_gateway(self): - self.addCleanup(self._denitialize_network) - self.openstack('nat gateway delete ' + self.NAT_ID) - - def create_snat_rule(self): - nat_gateway = self.create_nat_gateway() - self.assertIsNotNone(nat_gateway) - self.assertIsNotNone(self.FLOATING_IP_ID) - json_output = json.loads(self.openstack( - 'nat snat rule create ' - '--nat-gateway-id {nat_gateway_id} ' - '--floating-ip-id {floating_ip_id} ' - '--network-id {net_id} -f json'.format( - nat_gateway_id=nat_gateway['id'], - floating_ip_id=self.FLOATING_IP_ID, - net_id=self.NETWORK_ID) - )) - self.assertIsNotNone(json_output) - self.SNAT_RULE_ID = json_output['id'] - return json_output - - def delete_snat_rule(self): - self.addCleanup(self.delete_nat_gateway) - self.openstack( - 'nat snat rule delete ' + self.SNAT_RULE_ID) - - def create_dnat_rule(self): - nat_gateway = self.create_nat_gateway() - self.assertIsNotNone(nat_gateway) - self.assertIsNotNone(self.FLOATING_IP_ID) - json_output = json.loads(self.openstack( - 'nat dnat rule create ' - '--nat-gateway-id {nat_gateway_id} ' - '--floating-ip-id {floating_ip_id} ' - '--protocol {protocol} ' - '--internal-service-port 80 ' - '--external-service-port 80 ' - '--private-ip {private_ip} ' - '-f json'.format( - nat_gateway_id=nat_gateway['id'], - protocol='TCP', - private_ip='192.168.0.3', - floating_ip_id=self.FLOATING_IP_ID) - )) - self.assertIsNotNone(json_output) - self.DNAT_RULE_ID = json_output['id'] - return json_output - - def delete_dnat_rule(self): - self.addCleanup(self.delete_nat_gateway) - self.openstack( - 'nat dnat rule delete ' + self.DNAT_RULE_ID) - - def _initialize_network(self): - router = json.loads(self.openstack( - 'router create -f json ' + self.ROUTER_NAME - )) - net = json.loads(self.openstack( - 'network create -f json ' + self.NETWORK_NAME - )) - self.openstack( - 'subnet create {subnet} -f json ' - '--network {net} ' - '--subnet-range 192.168.0.0/24 '.format( - subnet=self.SUBNET_NAME, - net=self.NETWORK_NAME - )) - - self.openstack( - 'router add subnet {router} ' - '{subnet} '.format( - router=self.ROUTER_NAME, - subnet=self.SUBNET_NAME - ) - ) - - floating_ip = json.loads(self.openstack( - 'floating ip create -f json ' - '{network}'.format( - network='admin_external_net') - )) - - self.ROUTER_ID = router['id'] - self.NETWORK_ID = net['id'] - self.FLOATING_IP_ID = floating_ip['id'] - - def _denitialize_network(self): - self.openstack( - 'floating ip delete ' + self.FLOATING_IP_ID - ) - self.openstack( - 'router remove subnet {router} ' - '{subnet} '.format( - router=self.ROUTER_NAME, - subnet=self.SUBNET_NAME - ) - ) - self.openstack( - 'subnet delete ' + self.SUBNET_NAME - ) - self.openstack( - 'network delete ' + self.NETWORK_NAME - ) - self.openstack( - 'router delete ' + self.ROUTER_NAME - ) diff --git a/otcextensions/tests/functional/osclient/privatenat/v3/test_gateway.py b/otcextensions/tests/functional/osclient/privatenat/v3/test_private_nat_gateway.py similarity index 84% rename from otcextensions/tests/functional/osclient/privatenat/v3/test_gateway.py rename to otcextensions/tests/functional/osclient/privatenat/v3/test_private_nat_gateway.py index bb0e70bc1..aed48e4ff 100644 --- a/otcextensions/tests/functional/osclient/privatenat/v3/test_gateway.py +++ b/otcextensions/tests/functional/osclient/privatenat/v3/test_private_nat_gateway.py @@ -12,14 +12,14 @@ import json -from otcextensions.tests.functional.osclient.privatenat.v3 import common +from openstackclient.tests.functional import base -class TestGateway(common.PrivateNatTestCase): +class TestPrivateNatGateway(base.TestCase): """Functional Tests for Private NAT Gateway""" def setUp(self): - super(TestGateway, self).setUp() + super(TestPrivateNatGateway, self).setUp() def test_private_nat_gateway_list(self): json_output = json.loads(self.openstack(