From b988dd943468af3f43bdde106e897ec48d7a89ba Mon Sep 17 00:00:00 2001 From: wuebbels <47345655+wuebbels@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:38:45 +0200 Subject: [PATCH] complete implementation of vpc endpoint (#422) complete implementation of vpc endpoint Reviewed-by: Anton Sidelnikov Reviewed-by: Vineet Pruthi --- doc/source/cli/index.rst | 1 + doc/source/cli/vpcep.rst | 35 ++ doc/source/coverage.rst | 6 + doc/source/sdk/guides/index.rst | 1 + doc/source/sdk/guides/vpcep.rst | 166 ++++++ doc/source/sdk/proxies/index.rst | 1 + doc/source/sdk/proxies/vpcep.rst | 47 ++ doc/source/sdk/resources/index.rst | 1 + doc/source/sdk/resources/vpcep/index.rst | 11 + .../sdk/resources/vpcep/v1/connection.rst | 13 + .../sdk/resources/vpcep/v1/endpoint.rst | 13 + doc/source/sdk/resources/vpcep/v1/quota.rst | 13 + doc/source/sdk/resources/vpcep/v1/service.rst | 13 + .../sdk/resources/vpcep/v1/whitelist.rst | 13 + examples/vpcep/create_endpoint.py | 28 ++ examples/vpcep/create_service.py | 30 ++ examples/vpcep/delete_endpoint.py | 20 + examples/vpcep/delete_service.py | 20 + examples/vpcep/find_service.py | 21 + examples/vpcep/get_endpoint.py | 21 + examples/vpcep/get_service.py | 21 + examples/vpcep/list_endpoints.py | 20 + examples/vpcep/list_resource_quota.py | 20 + examples/vpcep/list_service_connections.py | 23 + examples/vpcep/list_service_whitelist.py | 22 + examples/vpcep/list_services.py | 20 + examples/vpcep/manage_service_connections.py | 27 + examples/vpcep/manage_service_whitelist.py | 27 + otcextensions/osclient/vpcep/__init__.py | 0 otcextensions/osclient/vpcep/client.py | 53 ++ otcextensions/osclient/vpcep/v1/__init__.py | 0 otcextensions/osclient/vpcep/v1/connection.py | 178 +++++++ otcextensions/osclient/vpcep/v1/endpoint.py | 286 +++++++++++ otcextensions/osclient/vpcep/v1/quota.py | 62 +++ otcextensions/osclient/vpcep/v1/service.py | 416 +++++++++++++++ otcextensions/osclient/vpcep/v1/whitelist.py | 157 ++++++ otcextensions/sdk/vpcep/v1/_proxy.py | 268 +++++++++- otcextensions/sdk/vpcep/v1/connection.py | 66 +++ otcextensions/sdk/vpcep/v1/endpoint.py | 179 +++++++ otcextensions/sdk/vpcep/v1/quota.py | 113 +++++ otcextensions/sdk/vpcep/v1/service.py | 113 +++++ otcextensions/sdk/vpcep/v1/whitelist.py | 62 +++ otcextensions/sdk/vpcep/vpcep_service.py | 9 +- .../tests/unit/osclient/vpcep/__init__.py | 0 .../tests/unit/osclient/vpcep/v1/__init__.py | 0 .../tests/unit/osclient/vpcep/v1/fakes.py | 161 ++++++ .../unit/osclient/vpcep/v1/test_connection.py | 187 +++++++ .../unit/osclient/vpcep/v1/test_endpoint.py | 379 ++++++++++++++ .../unit/osclient/vpcep/v1/test_quota.py | 72 +++ .../unit/osclient/vpcep/v1/test_service.py | 474 ++++++++++++++++++ .../unit/osclient/vpcep/v1/test_whitelist.py | 185 +++++++ otcextensions/tests/unit/sdk/base.py | 60 ++- otcextensions/tests/unit/sdk/utils.py | 33 ++ .../tests/unit/sdk/vpcep/__init__.py | 0 .../tests/unit/sdk/vpcep/v1/__init__.py | 0 .../unit/sdk/vpcep/v1/test_connection.py | 112 +++++ .../tests/unit/sdk/vpcep/v1/test_endpoint.py | 97 ++++ .../tests/unit/sdk/vpcep/v1/test_proxy.py | 158 ++++++ .../tests/unit/sdk/vpcep/v1/test_quota.py | 50 ++ .../tests/unit/sdk/vpcep/v1/test_service.py | 80 +++ .../tests/unit/sdk/vpcep/v1/test_whitelist.py | 100 ++++ .../notes/vpcep-module-d76cfde52e8e1711.yaml | 5 + setup.cfg | 17 + 63 files changed, 4759 insertions(+), 27 deletions(-) create mode 100644 doc/source/cli/vpcep.rst create mode 100644 doc/source/sdk/guides/vpcep.rst create mode 100644 doc/source/sdk/proxies/vpcep.rst create mode 100644 doc/source/sdk/resources/vpcep/index.rst create mode 100644 doc/source/sdk/resources/vpcep/v1/connection.rst create mode 100644 doc/source/sdk/resources/vpcep/v1/endpoint.rst create mode 100644 doc/source/sdk/resources/vpcep/v1/quota.rst create mode 100644 doc/source/sdk/resources/vpcep/v1/service.rst create mode 100644 doc/source/sdk/resources/vpcep/v1/whitelist.rst create mode 100644 examples/vpcep/create_endpoint.py create mode 100644 examples/vpcep/create_service.py create mode 100644 examples/vpcep/delete_endpoint.py create mode 100644 examples/vpcep/delete_service.py create mode 100644 examples/vpcep/find_service.py create mode 100644 examples/vpcep/get_endpoint.py create mode 100644 examples/vpcep/get_service.py create mode 100644 examples/vpcep/list_endpoints.py create mode 100644 examples/vpcep/list_resource_quota.py create mode 100644 examples/vpcep/list_service_connections.py create mode 100644 examples/vpcep/list_service_whitelist.py create mode 100644 examples/vpcep/list_services.py create mode 100644 examples/vpcep/manage_service_connections.py create mode 100644 examples/vpcep/manage_service_whitelist.py create mode 100644 otcextensions/osclient/vpcep/__init__.py create mode 100644 otcextensions/osclient/vpcep/client.py create mode 100644 otcextensions/osclient/vpcep/v1/__init__.py create mode 100644 otcextensions/osclient/vpcep/v1/connection.py create mode 100644 otcextensions/osclient/vpcep/v1/endpoint.py create mode 100644 otcextensions/osclient/vpcep/v1/quota.py create mode 100644 otcextensions/osclient/vpcep/v1/service.py create mode 100644 otcextensions/osclient/vpcep/v1/whitelist.py create mode 100644 otcextensions/sdk/vpcep/v1/connection.py create mode 100644 otcextensions/sdk/vpcep/v1/endpoint.py create mode 100644 otcextensions/sdk/vpcep/v1/quota.py create mode 100644 otcextensions/sdk/vpcep/v1/service.py create mode 100644 otcextensions/sdk/vpcep/v1/whitelist.py create mode 100644 otcextensions/tests/unit/osclient/vpcep/__init__.py create mode 100644 otcextensions/tests/unit/osclient/vpcep/v1/__init__.py create mode 100644 otcextensions/tests/unit/osclient/vpcep/v1/fakes.py create mode 100644 otcextensions/tests/unit/osclient/vpcep/v1/test_connection.py create mode 100644 otcextensions/tests/unit/osclient/vpcep/v1/test_endpoint.py create mode 100644 otcextensions/tests/unit/osclient/vpcep/v1/test_quota.py create mode 100644 otcextensions/tests/unit/osclient/vpcep/v1/test_service.py create mode 100644 otcextensions/tests/unit/osclient/vpcep/v1/test_whitelist.py create mode 100644 otcextensions/tests/unit/sdk/utils.py create mode 100644 otcextensions/tests/unit/sdk/vpcep/__init__.py create mode 100644 otcextensions/tests/unit/sdk/vpcep/v1/__init__.py create mode 100644 otcextensions/tests/unit/sdk/vpcep/v1/test_connection.py create mode 100644 otcextensions/tests/unit/sdk/vpcep/v1/test_endpoint.py create mode 100644 otcextensions/tests/unit/sdk/vpcep/v1/test_proxy.py create mode 100644 otcextensions/tests/unit/sdk/vpcep/v1/test_quota.py create mode 100644 otcextensions/tests/unit/sdk/vpcep/v1/test_service.py create mode 100644 otcextensions/tests/unit/sdk/vpcep/v1/test_whitelist.py create mode 100644 releasenotes/notes/vpcep-module-d76cfde52e8e1711.yaml diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 11935ac3f..f43d033ea 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -47,3 +47,4 @@ documentation of these services: smn volume_backup vpc + vpcep diff --git a/doc/source/cli/vpcep.rst b/doc/source/cli/vpcep.rst new file mode 100644 index 000000000..d7d4aea23 --- /dev/null +++ b/doc/source/cli/vpcep.rst @@ -0,0 +1,35 @@ +VPC Endpoint Service (VPCEP) +============================ + +The VPCEP client is the command-line interface (CLI) for +the VPC Endpoint Service (VPCEP) API and its extensions. + +For help on a specific `vpcep` command, enter: + +.. code-block:: console + + $ openstack vpcep help SUBCOMMAND + +.. _vpcep_endpoint: + +Vpcep Endpoint Operations +------------------------- + +.. autoprogram-cliff:: openstack.vpcep.v1 + :command: vpcep endpoint * + +.. _vpcep_service: + +Vpcep Service Operations +------------------------ + +.. autoprogram-cliff:: openstack.vpcep.v1 + :command: vpcep service * + +.. _vpcep_quota: + +Vpcep Quota Operations +---------------------- + +.. autoprogram-cliff:: openstack.vpcep.v1 + :command: vpcep quota * diff --git a/doc/source/coverage.rst b/doc/source/coverage.rst index abb9ab918..05db93d3b 100644 --- a/doc/source/coverage.rst +++ b/doc/source/coverage.rst @@ -137,3 +137,9 @@ under several service tags. This may change in future. - X - X - + * - vpcep + - VPC Endpoint Service + - X + - X + - X + - diff --git a/doc/source/sdk/guides/index.rst b/doc/source/sdk/guides/index.rst index 4350bf006..06e20f8b7 100644 --- a/doc/source/sdk/guides/index.rst +++ b/doc/source/sdk/guides/index.rst @@ -29,6 +29,7 @@ Open Telekom Cloud related User Guides sfsturbo smn vpc + vpcep logging .. _user_guides: diff --git a/doc/source/sdk/guides/vpcep.rst b/doc/source/sdk/guides/vpcep.rst new file mode 100644 index 000000000..ff2bf9a94 --- /dev/null +++ b/doc/source/sdk/guides/vpcep.rst @@ -0,0 +1,166 @@ +VPC Endpoint (VPCEP) +==================== + +VPC Endpoint (VPCEP) is a cloud service that provides secure and +private channels to connect your VPCs to VPC endpoint services, +including cloud services or your private services. It allows you +to plan networks flexibly without having to use EIPs. There are two +types of resources: VPC endpoint services and VPC endpoints. + +.. contents:: Table of Contents + :local: + +VPC Endpoint Service +-------------------- + +VPC endpoint services are cloud services or private services that +you manually configure in VPCEP. You can access these endpoint +services using VPC endpoints. + +List Services +^^^^^^^^^^^^^ + +This interface is used to query an VPCEP services list and to filter +the output with query parameters. +:class:`~otcextensions.sdk.vpcep.v2.service.Service`. + +.. literalinclude:: ../examples/vpcep/list_endpoints.py + :lines: 14-20 + +Create Service +^^^^^^^^^^^^^^ + +This interface is used to create a VPCEP service with +parameters. +:class:`~otcextensions.sdk.vpcep.v2.service.Service`. + +.. literalinclude:: ../examples/vpcep/create_service.py + :lines: 14-30 + +Get Service +^^^^^^^^^^^ + +This interface is used to get a VPCEP service by ID +or an instance of class. +:class:`~otcextensions.sdk.vpcep.v2.service.Service`. + +.. literalinclude:: ../examples/vpcep/get_service.py + :lines: 14-21 + +Find Service +^^^^^^^^^^^^ + +This interface is used to find a VPCEP service by ID +or name. +:class:`~otcextensions.sdk.vpcep.v2.service.Service`. + +.. literalinclude:: ../examples/vpcep/find_service.py + :lines: 14-21 + +Delete Service +^^^^^^^^^^^^^^ + +This interface is used to delete a VPCEP service by ID +or an instance of class +:class:`~otcextensions.sdk.vpcep.v2.service.Service`. + +.. literalinclude:: ../examples/vpcep/delete_service.py + :lines: 14-20 + +List Service Whitelist +^^^^^^^^^^^^^^^^^^^^^^ + +This interface is used to query an VPCEP service whitelist and to filter +the output with query parameters. +:class:`~otcextensions.sdk.vpcep.v2.whitelist.Whitelist`. + +.. literalinclude:: ../examples/vpcep/list_service_whitelist.py + :lines: 14-22 + +Manage Service Whitelist +^^^^^^^^^^^^^^^^^^^^^^^^ + +This interface is used to manage a VPCEP service whitelist. +:class:`~otcextensions.sdk.vpcep.v2.whitelist.Whitelist`. + +.. literalinclude:: ../examples/vpcep/manage_service_whitelist.py + :lines: 14-27 + +List Service Connections +^^^^^^^^^^^^^^^^^^^^^^^^ + +This interface is used to query an VPCEP service connections and to filter +the output with query parameters. +:class:`~otcextensions.sdk.vpcep.v2.connection.Connection`. + +.. literalinclude:: ../examples/vpcep/list_service_connections.py + :lines: 14-22 + +Manage Service Connections +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This interface is used to manage a VPCEP service connections. +:class:`~otcextensions.sdk.vpcep.v2.connection.Connection`. + +.. literalinclude:: ../examples/vpcep/manage_service_connections.py + :lines: 14-27 + +VPC Endpoint +------------ + +VPC endpoints are secure and private channels for connecting +VPCs to VPC endpoint services. + +List Endpoints +^^^^^^^^^^^^^^ + +This interface is used to query an VPC endpoint list and to filter +the output with query parameters. +:class:`~otcextensions.sdk.vpcep.v2.endpoint.Endpoint`. + +.. literalinclude:: ../examples/vpcep/list_endpoints.py + :lines: 14-20 + +Create Endpoint +^^^^^^^^^^^^^^^ + +This interface is used to create a VPC endpoint with +parameters. +:class:`~otcextensions.sdk.vpcep.v2.endpoint.Endpoint`. + +.. literalinclude:: ../examples/vpcep/create_endpoint.py + :lines: 14-28 + +Get Endpoint +^^^^^^^^^^^^ + +This interface is used to get a VPC endpoint by ID +or an instance of class +:class:`~otcextensions.sdk.vpcep.v2.endpoint.Endpoint`. + +.. literalinclude:: ../examples/vpcep/get_endpoint.py + :lines: 14-21 + +Delete Endpoint +^^^^^^^^^^^^^^^ + +This interface is used to delete a VPC endpoint by ID +or an instance of class +:class:`~otcextensions.sdk.vpcep.v2.endpoint.Endpoint`. + +.. literalinclude:: ../examples/vpcep/delete_endpoint.py + :lines: 14-20 + +VPCEP Quota +----------- + +List Resource Quota +^^^^^^^^^^^^^^^^^^^ + +This interface is used to query quota of vpc endpoint and endpoint_service +on a specific tenant. +:class:`~otcextensions.sdk.vpcep.v2.quota.Quota`. + +.. literalinclude:: ../examples/vpcep/list_resource_quota.py + :lines: 14-20 + diff --git a/doc/source/sdk/proxies/index.rst b/doc/source/sdk/proxies/index.rst index 4bf89dccc..14ec153d5 100644 --- a/doc/source/sdk/proxies/index.rst +++ b/doc/source/sdk/proxies/index.rst @@ -35,6 +35,7 @@ Service Proxies Software Repository for Containers Service (SWR) Volume Backup Service (VBS) Virtual Private Cloud (VPC) + VPC Endpoint (VPCEP) Web Application Firewall (WAF) .. _service-proxies: diff --git a/doc/source/sdk/proxies/vpcep.rst b/doc/source/sdk/proxies/vpcep.rst new file mode 100644 index 000000000..3c870e9ce --- /dev/null +++ b/doc/source/sdk/proxies/vpcep.rst @@ -0,0 +1,47 @@ +VPCEP API +========= + +.. automodule:: otcextensions.sdk.vpcep.v1._proxy + +The VPC Endpoint Class +---------------------- + +The vpcep high-level interface is available through the ``vpcep`` +member of a :class:`~openstack.connection.Connection` object. The +``vpcep`` member will only be added if the +``otcextensions.sdk.register_otc_extensions(conn)`` method is called. + +Endpoint Operations +^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.vpcep.v1._proxy.Proxy + :noindex: + :members: endpoints, create_endpoint, get_endpoint, delete_endpoint + +Service Operations +^^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.vpcep.v1._proxy.Proxy + :noindex: + :members: services, create_service, get_service, find_service, delete_service + +Service Connection Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.vpcep.v1._proxy.Proxy + :noindex: + :members: service_connections, manage_service_connections + +Service WhiteList Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.vpcep.v1._proxy.Proxy + :noindex: + :members: service_whitelist, manage_service_whitelist + +Quota Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.vpcep.v1._proxy.Proxy + :noindex: + :members: resource_quota diff --git a/doc/source/sdk/resources/index.rst b/doc/source/sdk/resources/index.rst index 2a63a96d5..db9ba37e5 100644 --- a/doc/source/sdk/resources/index.rst +++ b/doc/source/sdk/resources/index.rst @@ -35,6 +35,7 @@ Open Telekom Cloud Resources Storage Disaster Recovery Service (SDRS) Software Repository for Containers Service (SWR) Virtual Private Cloud (VPC) + VPC Endpoint (VPCEP) Web Application Firewall (WAF) Every resource which is used within the proxy methods have own attributes. diff --git a/doc/source/sdk/resources/vpcep/index.rst b/doc/source/sdk/resources/vpcep/index.rst new file mode 100644 index 000000000..bdbcd89e7 --- /dev/null +++ b/doc/source/sdk/resources/vpcep/index.rst @@ -0,0 +1,11 @@ +VPCEP Resources +=============== + +.. toctree:: + :maxdepth: 1 + + v1/endpoint + v1/service + v1/connection + v1/whitelist + v1/quota diff --git a/doc/source/sdk/resources/vpcep/v1/connection.rst b/doc/source/sdk/resources/vpcep/v1/connection.rst new file mode 100644 index 000000000..066533692 --- /dev/null +++ b/doc/source/sdk/resources/vpcep/v1/connection.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.vpecp.v1.connection +===================================== + +.. automodule:: otcextensions.sdk.vpcep.v1.connection + +The VPCEP Connection Class +-------------------------- + +The ``Connection`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.vpcep.v1.connection.Connection + :members: diff --git a/doc/source/sdk/resources/vpcep/v1/endpoint.rst b/doc/source/sdk/resources/vpcep/v1/endpoint.rst new file mode 100644 index 000000000..ae55fc08f --- /dev/null +++ b/doc/source/sdk/resources/vpcep/v1/endpoint.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.vpecp.v1.endpoint +=================================== + +.. automodule:: otcextensions.sdk.vpcep.v1.endpoint + +The VPCEP Endpoint Class +------------------------ + +The ``Endpoint`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.vpcep.v1.endpoint.Endpoint + :members: diff --git a/doc/source/sdk/resources/vpcep/v1/quota.rst b/doc/source/sdk/resources/vpcep/v1/quota.rst new file mode 100644 index 000000000..862f87994 --- /dev/null +++ b/doc/source/sdk/resources/vpcep/v1/quota.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.vpecp.v1.quota +==================================== + +.. automodule:: otcextensions.sdk.vpcep.v1.quota + +The VPCEP Quota Class +--------------------- + +The ``Quota`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.vpcep.v1.quota.Quota + :members: diff --git a/doc/source/sdk/resources/vpcep/v1/service.rst b/doc/source/sdk/resources/vpcep/v1/service.rst new file mode 100644 index 000000000..2569e9ac4 --- /dev/null +++ b/doc/source/sdk/resources/vpcep/v1/service.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.vpecp.v1.service +================================== + +.. automodule:: otcextensions.sdk.vpcep.v1.service + +The VPCEP Service Class +----------------------- + +The ``Service`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.vpcep.v1.service.Service + :members: diff --git a/doc/source/sdk/resources/vpcep/v1/whitelist.rst b/doc/source/sdk/resources/vpcep/v1/whitelist.rst new file mode 100644 index 000000000..5009fbd16 --- /dev/null +++ b/doc/source/sdk/resources/vpcep/v1/whitelist.rst @@ -0,0 +1,13 @@ +otcextensions.sdk.vpecp.v1.whitelist +==================================== + +.. automodule:: otcextensions.sdk.vpcep.v1.whitelist + +The VPCEP Whitelist Class +------------------------- + +The ``Whitelist`` class inherits from +:class:`~otcextensions.sdk.sdk_resource.Resource`. + +.. autoclass:: otcextensions.sdk.vpcep.v1.whitelist.Whitelist + :members: diff --git a/examples/vpcep/create_endpoint.py b/examples/vpcep/create_endpoint.py new file mode 100644 index 000000000..a0ed23d63 --- /dev/null +++ b/examples/vpcep/create_endpoint.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. +# +"""Create VPC Endpoint.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +attrs = { + 'network_id': 'network-uuid', + 'router_id': 'router-uuid', + 'tags': [{'key': 'test1', 'value': 'test1'}], + 'endpoint_service_id': 'endpoint-service-uuid', + 'enable_dns': True, +} + +endpoint = conn.vpcep.create_endpoint(**attrs) +print(endpoint) diff --git a/examples/vpcep/create_service.py b/examples/vpcep/create_service.py new file mode 100644 index 000000000..cb4d99389 --- /dev/null +++ b/examples/vpcep/create_service.py @@ -0,0 +1,30 @@ +# 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. +# +"""Create VPC Endpoint Service.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +attrs = { + 'port_id': 'port-id', + 'vpc_id': 'router-id', + 'service_name': 'test-service', + 'approval_enabled': False, + 'service_type': 'interface', + 'server_type': 'VM', + 'ports': [{'client_port': 8080, 'server_port': 90, 'protocol': 'TCP'}], +} + +endpoint_service = conn.vpcep.create_service(**attrs) +print(endpoint_service) diff --git a/examples/vpcep/delete_endpoint.py b/examples/vpcep/delete_endpoint.py new file mode 100644 index 000000000..6a4d4f298 --- /dev/null +++ b/examples/vpcep/delete_endpoint.py @@ -0,0 +1,20 @@ +# 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. +# +"""Delete VPC Endpoint.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +endpoint_id = 'endpoint-uuid' +conn.vpcep.delete_endpoint(endpoint_id, ignore_missing=False) diff --git a/examples/vpcep/delete_service.py b/examples/vpcep/delete_service.py new file mode 100644 index 000000000..40c7f2793 --- /dev/null +++ b/examples/vpcep/delete_service.py @@ -0,0 +1,20 @@ +# 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. +# +"""Delete VPC Endpoint Service.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +endpoint_service_id = 'endpoint-service-uuid' +conn.vpcep.delete_service(endpoint_service_id, ignore_missing=False) diff --git a/examples/vpcep/find_service.py b/examples/vpcep/find_service.py new file mode 100644 index 000000000..332ea27e6 --- /dev/null +++ b/examples/vpcep/find_service.py @@ -0,0 +1,21 @@ +# 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. +# +"""Get VPC Endpoint Service by name or Id.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +name_or_id = 'xyz' +endpoint_service = conn.vpcep.find_service(name_or_id, ignore_missing=False) +print(endpoint_service) diff --git a/examples/vpcep/get_endpoint.py b/examples/vpcep/get_endpoint.py new file mode 100644 index 000000000..7a03fa192 --- /dev/null +++ b/examples/vpcep/get_endpoint.py @@ -0,0 +1,21 @@ +# 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. +# +"""Get VPC Endpoint details.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +endpoint_id = 'endpoint-uuid' +endpoint = conn.vpcep.get_endpoint(endpoint_id) +print(endpoint) diff --git a/examples/vpcep/get_service.py b/examples/vpcep/get_service.py new file mode 100644 index 000000000..fe1ecf318 --- /dev/null +++ b/examples/vpcep/get_service.py @@ -0,0 +1,21 @@ +# 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. +# +"""Get VPC Endpoint Service.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +endpoint_service = 'endpoint-service-uuid' +endpoint_service = conn.vpcep.get_service(endpoint_service) +print(endpoint_service) diff --git a/examples/vpcep/list_endpoints.py b/examples/vpcep/list_endpoints.py new file mode 100644 index 000000000..4b769d2f0 --- /dev/null +++ b/examples/vpcep/list_endpoints.py @@ -0,0 +1,20 @@ +# 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 endpoints.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +endpoints = conn.vpcep.endpoints() +print(list(endpoints)) diff --git a/examples/vpcep/list_resource_quota.py b/examples/vpcep/list_resource_quota.py new file mode 100644 index 000000000..f8197f559 --- /dev/null +++ b/examples/vpcep/list_resource_quota.py @@ -0,0 +1,20 @@ +# 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 vpcep resource quota.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +quota = conn.vpcep.resource_quota() +print(list(quota)) diff --git a/examples/vpcep/list_service_connections.py b/examples/vpcep/list_service_connections.py new file mode 100644 index 000000000..e4ced567e --- /dev/null +++ b/examples/vpcep/list_service_connections.py @@ -0,0 +1,23 @@ +# 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 endpoint service connections.""" + +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +name_or_id = 'xyz' +endpoint_service = conn.vpcep.find_service(name_or_id) +connections = conn.vpcep.service_connections(endpoint_service) +print(list(connections)) diff --git a/examples/vpcep/list_service_whitelist.py b/examples/vpcep/list_service_whitelist.py new file mode 100644 index 000000000..ec0377c74 --- /dev/null +++ b/examples/vpcep/list_service_whitelist.py @@ -0,0 +1,22 @@ +# 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 endpoint service whitelist.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +name_or_id = 'xyz' +endpoint_service = conn.vpcep.find_service(name_or_id) +whitelist = conn.vpcep.service_whitelist(endpoint_service) +print(list(whitelist)) diff --git a/examples/vpcep/list_services.py b/examples/vpcep/list_services.py new file mode 100644 index 000000000..3fea37003 --- /dev/null +++ b/examples/vpcep/list_services.py @@ -0,0 +1,20 @@ +# 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 endpoint services.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +endpoint_services = conn.vpcep.services() +print(list(endpoint_services)) diff --git a/examples/vpcep/manage_service_connections.py b/examples/vpcep/manage_service_connections.py new file mode 100644 index 000000000..c85fbc1cb --- /dev/null +++ b/examples/vpcep/manage_service_connections.py @@ -0,0 +1,27 @@ +# 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. +# +"""Manage endpoint service connections.""" + +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +name_or_id = 'xyz' +action = 'accept' +endpoints = ['endpoint1-id', 'endpoint2-id'] +endpoint_service = conn.vpcep.find_service(name_or_id) +connections = conn.vpcep.service_connections( + endpoint_service, action, endpoints +) +print(list(connections)) diff --git a/examples/vpcep/manage_service_whitelist.py b/examples/vpcep/manage_service_whitelist.py new file mode 100644 index 000000000..1ed1d63ce --- /dev/null +++ b/examples/vpcep/manage_service_whitelist.py @@ -0,0 +1,27 @@ +# 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. +# +"""Manage endpoint service whitelist.""" +import openstack + +openstack.enable_logging(True) +conn = openstack.connect(cloud='otc') + +name_or_id = 'xyz' +action = 'add' +domains = ['domain1-id', 'domain2-id'] + +endpoint_service = conn.vpcep.find_service(name_or_id) +whitelist = conn.vpcep.service_whitelist( + endpoint_service, action, domains +) +print(list(whitelist)) diff --git a/otcextensions/osclient/vpcep/__init__.py b/otcextensions/osclient/vpcep/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/osclient/vpcep/client.py b/otcextensions/osclient/vpcep/client.py new file mode 100644 index 000000000..958e3dd82 --- /dev/null +++ b/otcextensions/osclient/vpcep/client.py @@ -0,0 +1,53 @@ +# 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 = '1' +API_VERSION_OPTION = 'os_vpcep_api_version' +API_NAME = "vpcep" +API_VERSIONS = { + "1": "openstack.connection.Connection" +} + + +def make_client(instance): + """Returns a vpcep proxy""" + + conn = instance.sdk_connection + + if getattr(conn, 'vpcep', None) is None: + LOG.debug('OTC extensions are not registered. Do that now') + sdk.register_otc_extensions(conn) + + LOG.debug('VPC Endpoint client initialized using OpenStack OTC SDK: %s', + conn.vpcep) + return conn.vpcep + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-vpcep-api-version', + metavar='', + default=utils.env('OS_VPCEP_API_VERSION'), + help=_("VPCEP API version, default=%s " + "(Env: OS_VPCEP_API_VERSION)") % DEFAULT_API_VERSION + ) + return parser diff --git a/otcextensions/osclient/vpcep/v1/__init__.py b/otcextensions/osclient/vpcep/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/osclient/vpcep/v1/connection.py b/otcextensions/osclient/vpcep/v1/connection.py new file mode 100644 index 000000000..2a4c99e82 --- /dev/null +++ b/otcextensions/osclient/vpcep/v1/connection.py @@ -0,0 +1,178 @@ +# 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. +# +"""VPC Endpoint Service v1 action implementations""" +import logging + +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.common import sdk_utils +from otcextensions.i18n import _ + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = {} + hidden = [ + 'location', + ] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden + ) + + +class ListConnections(command.Lister): + + _description = _('List VPC Endpoint Service Connections.') + columns = ( + 'Id', + 'Domain Id', + 'Status', + 'Created At', + 'Updated At', + ) + + def get_parser(self, prog_name): + parser = super(ListConnections, self).get_parser(prog_name) + + parser.add_argument( + 'service', + metavar='', + help=_('ID or name of the VPC Endpoint Service.'), + ) + parser.add_argument( + '--id', + metavar='', + help=_('VPC Endpoint ID.'), + ) + parser.add_argument( + '--marker-id', + metavar='', + help=_('Packet ID of the VPC endpoint.'), + ) + parser.add_argument( + '--sort-key', + metavar='{created_at, updated_at}', + type=lambda s: s.lower(), + choices=['created_at', 'updated_at'], + help=_('Sorting field of the VPC endpoint service list.'), + ) + parser.add_argument( + '--sort-dir', + metavar='{asc, desc}', + type=lambda s: s.lower(), + choices=['asc', 'desc'], + help=_('Sorting order of the VPC endpoint service list.'), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Limit number of endpoint connections queried.'), + ) + parser.add_argument( + '--offset', + metavar='', + type=int, + help=_('Connection records after this Offset will be queried.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + args_list = [ + 'id', + 'marker_id', + 'limit', + 'offset', + 'sort_key', + 'sort_dir', + ] + attrs = {} + for arg in args_list: + val = getattr(parsed_args, arg) + if val: + attrs[arg] = val + + endpoint_service = client.find_service(parsed_args.service) + data = client.service_connections(endpoint_service, **attrs) + + return ( + self.columns, + (utils.get_item_properties(s, self.columns) for s in data), + ) + + +class ManageConnections(command.Lister): + _description = _('Manage VPC Endpoint Service Connections.') + + columns = ( + 'Id', + 'Domain Id', + 'Status', + 'Created At', + 'Updated At', + ) + + def get_parser(self, prog_name): + parser = super(ManageConnections, self).get_parser(prog_name) + + parser.add_argument( + 'service', + metavar='', + help=_('ID or name of the VPC Endpoint Service.'), + ) + parser.add_argument( + 'endpoint', + metavar='', + nargs='+', + help=_( + 'VPC Endpoint(s) ID to Accept Or Reject ' + 'Connection to Service.' + ), + ) + manage_request_group = parser.add_mutually_exclusive_group( + required=True + ) + manage_request_group.add_argument( + '--accept', + action='store_true', + dest='receive', + help=('Accept VPC Endpoint Connection to Endpoint Service.'), + ) + manage_request_group.add_argument( + '--reject', + action='store_true', + help=('Reject VPC Endpoint Connection to Endpoint Service.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + set_args = ('receive', 'reject') + request_status = [ + request for request in set_args if getattr(parsed_args, request) + ] + + endpoint_service = client.find_service(parsed_args.service) + data = client.manage_service_connections( + endpoint_service, + endpoints=parsed_args.endpoint, + action=request_status[0], + ) + + return ( + self.columns, + (utils.get_item_properties(s, self.columns) for s in data), + ) diff --git a/otcextensions/osclient/vpcep/v1/endpoint.py b/otcextensions/osclient/vpcep/v1/endpoint.py new file mode 100644 index 000000000..c5ede2fd5 --- /dev/null +++ b/otcextensions/osclient/vpcep/v1/endpoint.py @@ -0,0 +1,286 @@ +# 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. +# +"""VPC Endpoint Service v1 action implementations""" +import logging + +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.cli import format_columns +from osc_lib.cli import parseractions +from osc_lib.command import command + +from otcextensions.common import cli_utils +from otcextensions.common import sdk_utils +from otcextensions.i18n import _ + +LOG = logging.getLogger(__name__) + + +_formatters = { + 'active_status': format_columns.ListColumn, + 'tags': cli_utils.YamlFormat, + 'whitelist': format_columns.ListColumn, + 'route_tables': format_columns.ListColumn, +} + + +def _get_columns(item): + column_map = {} + hidden = [ + 'location', + ] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden + ) + + +class ListEndpoints(command.Lister): + + _description = _('List VPC Endpoints.') + columns = ('Id', 'Endpoint Service Name', 'Status', 'Is Enabled') + + def get_parser(self, prog_name): + parser = super(ListEndpoints, self).get_parser(prog_name) + + parser.add_argument( + '--id', + metavar='', + help=_('ID of the VPC endpoint.'), + ) + parser.add_argument( + '--service-name', + metavar='', + dest='endpoint_service_name', + help=_('Name of the VPC endpoint service.'), + ) + parser.add_argument( + '--sort-key', + metavar='{created_at, updated_at}', + type=lambda s: s.lower(), + choices=['created_at', 'updated_at'], + help=_('Sorting field of the VPC endpoint list.'), + ) + parser.add_argument( + '--sort-dir', + metavar='{asc, desc}', + type=lambda s: s.lower(), + choices=['asc', 'desc'], + help=_('Sorting order of the VPC endpoint list.'), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Limit number of VPC endpoints.'), + ) + parser.add_argument( + '--offset', + metavar='', + type=int, + help=_('Endpoints after this offset will be queried.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + args_list = [ + 'id', + 'endpoint_service_name', + 'limit', + 'offset', + 'sort_key', + 'sort_dir', + ] + params = {} + for arg in args_list: + val = getattr(parsed_args, arg) + if val: + params[arg] = val + + data = client.endpoints(**params) + + return ( + self.columns, + (utils.get_item_properties(s, self.columns) for s in data), + ) + + +class CreateEndpoint(command.ShowOne): + _description = _( + 'Create a VPC endpoint for accessing a VPC endpoint service.' + ) + + def get_parser(self, prog_name): + parser = super(CreateEndpoint, self).get_parser(prog_name) + parser.add_argument( + '--service-id', + metavar='', + dest='endpoint_service_id', + help=_('ID of the Vpc endpoint service.'), + ) + parser.add_argument( + '--router-id', + metavar='', + dest='vpc_id', + required=True, + help=_( + 'ID of the vpc/router where the VPC endpoint is to be created.' + ), + ) + parser.add_argument( + '--network-id', + metavar='', + dest='subnet_id', + required=True, + help=_('ID of the network created in the vpc/router.'), + ) + parser.add_argument( + '--port-ip', + metavar='', + help=_( + 'IP address for accessing the associated VPC endpoint service.' + ), + ) + parser.add_argument( + '--route-tables', + metavar='', + dest='routetables', + nargs='+', + help=_('Lists the IDs of route tables.'), + ) + parser.add_argument( + '--whitelist', + metavar='', + nargs='+', + help=_('Whitelist for controlling access to the VPC endpoint.'), + ) + parser.add_argument( + '--specification-name', + metavar='', + help=_('Name of the VPC endpoint specifications.'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('Description of the VPC endpoint.'), + ) + parser.add_argument( + '--tags', + metavar='key=,value=', + action=parseractions.MultiKeyValueAction, + dest='tags', + required_keys=['key', 'value'], + help=_( + 'Example: \n' + '--tags key=test-key,value=test-value\n' + 'Repeat option to provide multiple tags.' + ), + ) + parser.add_argument( + '--enable-dns', + action='store_true', + help=('Whether to create a private domain name. default (false)'), + ) + parser.add_argument( + '--enable-whitelist', + action='store_true', + help=('Whether access control is enabled.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + + args_list = [ + 'endpoint_service_id', + 'subnet_id', + 'vpc_id', + 'port_ip', + 'tags', + 'whitelist', + 'routetables', + 'enable_dns', + 'enable_whitelist', + 'specification_name', + 'description', + ] + attrs = {} + for arg in args_list: + val = getattr(parsed_args, arg) + if val: + attrs[arg] = val + + obj = client.create_endpoint(**attrs) + + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + + return (display_columns, data) + + +class ShowEndpoint(command.ShowOne): + _description = _('Show VPC endpoint details.') + + def get_parser(self, prog_name): + parser = super(ShowEndpoint, self).get_parser(prog_name) + parser.add_argument( + 'endpoint', + metavar='', + help=_('ID of the VPC endpoint.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + obj = client.get_endpoint(parsed_args.endpoint) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + + return (display_columns, data) + + +class DeleteEndpoint(command.Command): + + _description = _('Delete VPC endpoint(s).') + + def get_parser(self, prog_name): + parser = super(DeleteEndpoint, self).get_parser(prog_name) + parser.add_argument( + 'endpoint', + metavar='', + nargs='+', + help=_('ID of vpc endpoint(s) to delete.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + result = 0 + for endpoint in parsed_args.endpoint: + try: + client.delete_endpoint(endpoint) + except Exception as e: + result += 1 + LOG.error( + _( + 'Failed to delete VPC endpoint with ' + 'ID "%(endpoint)s": %(e)s' + ), + {'endpoint': endpoint, 'e': e}, + ) + if result > 0: + total = len(parsed_args.endpoint) + msg = _( + '%(result)s of %(total)s VPC endpoint(s) failed to delete.' + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) diff --git a/otcextensions/osclient/vpcep/v1/quota.py b/otcextensions/osclient/vpcep/v1/quota.py new file mode 100644 index 000000000..667241b3c --- /dev/null +++ b/otcextensions/osclient/vpcep/v1/quota.py @@ -0,0 +1,62 @@ +# 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. +# +"""VPC Endpoint Service v1 action implementations""" +import logging + +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.common import sdk_utils +from otcextensions.i18n import _ + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = {} + hidden = [ + 'location', + ] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden + ) + + +class ListQuota(command.Lister): + + _description = _('List VPC endpoint resource quotas.') + columns = ( + 'Type', + 'Quota', + 'Used', + ) + + def get_parser(self, prog_name): + parser = super(ListQuota, self).get_parser(prog_name) + parser.add_argument( + '--type', + metavar='{endpoint, endpoint_service}', + type=lambda s: s.lower(), + choices=['endpoint', 'endpoint_service'], + help=_('Specify the resource type.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + data = client.resource_quota(getattr(parsed_args, 'type')) + + return ( + self.columns, + (utils.get_item_properties(s, self.columns) for s in data), + ) diff --git a/otcextensions/osclient/vpcep/v1/service.py b/otcextensions/osclient/vpcep/v1/service.py new file mode 100644 index 000000000..dfa39bbea --- /dev/null +++ b/otcextensions/osclient/vpcep/v1/service.py @@ -0,0 +1,416 @@ +# 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. +# +"""VPC Endpoint Service v1 action implementations""" +import logging + +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.cli import parseractions +from osc_lib.command import command + +from otcextensions.common import cli_utils +from otcextensions.common import sdk_utils +from otcextensions.i18n import _ + +LOG = logging.getLogger(__name__) + +SERVER_TYPE_CHOICES = ['vm', 'vip', 'lb'] +SERVICE_TYPE_CHOICES = ['gateway', 'interface'] + + +_formatters = { + 'ports': cli_utils.YamlFormat, + 'tags': cli_utils.YamlFormat, +} + + +def _get_columns(item): + column_map = {} + hidden = [ + 'location', + ] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden + ) + + +def translate_response(func): + def new(self, *args, **kwargs): + obj = func(self, *args, **kwargs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + return (display_columns, data) + + new.__name__ = func.__name__ + new.__doc__ = func.__doc__ + return new + + +class ListServices(command.Lister): + + _description = _('List VPC Endpoint Services.') + columns = ( + 'Id', + 'Service Name', + 'Service Type', + 'Server Type', + 'Connection Count', + 'Status', + ) + + def get_parser(self, prog_name): + parser = super(ListServices, self).get_parser(prog_name) + + parser.add_argument( + '--id', + metavar='', + help=_('ID of the VPC Endpoint Service.'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the VPC Endpoint Service.'), + ) + parser.add_argument( + '--status', + metavar='', + help=_('Status of the VPC endpoint service.'), + ) + parser.add_argument( + '--sort-key', + metavar='{created_at, updated_at}', + type=lambda s: s.lower(), + choices=['created_at', 'updated_at'], + help=_('Sorting field of the VPC endpoint service list.'), + ) + parser.add_argument( + '--sort-dir', + metavar='{asc, desc}', + type=lambda s: s.lower(), + choices=['asc', 'desc'], + help=_('Sorting order of the VPC endpoint service list.'), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Limit number of VPC endpoint services displayed.'), + ) + parser.add_argument( + '--offset', + metavar='', + type=int, + help=_('Service records after this Offset will be queried.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + args_list = [ + 'id', + 'name', + 'limit', + 'offset', + 'sort_key', + 'sort_dir', + 'status', + ] + attrs = {} + for arg in args_list: + val = getattr(parsed_args, arg) + if val: + attrs[arg] = val + + data = client.services(**attrs) + + return ( + self.columns, + (utils.get_item_properties(s, self.columns) for s in data), + ) + + +class ShowService(command.ShowOne): + _description = _('Show VPC Endpoint Service Details.') + + def get_parser(self, prog_name): + parser = super(ShowService, self).get_parser(prog_name) + parser.add_argument( + 'service', + metavar='', + help=_('Name or ID of the VPC Endpoint Service.'), + ) + return parser + + @translate_response + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + return client.find_service(parsed_args.service) + + +class CreateService(command.ShowOne): + _description = _('Create new VPC Endpoint Service.') + + def get_parser(self, prog_name): + parser = super(CreateService, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Specifies name of the Endpoint Service.'), + ) + parser.add_argument( + '--port-id', + metavar='', + required=True, + help=_( + 'Specify the ID for identifying the backend ' + 'resource of the VPC endpoint service.' + ), + ) + parser.add_argument( + '--pool-id', + metavar='', + help=_( + 'Specify the ID of the cluster associated with ' + 'the target VPCEP resource.' + ), + ) + parser.add_argument( + '--router-id', + metavar='', + dest='vpc_id', + required=True, + help=_( + 'ID of the router (VPC) to which the backend resource ' + 'of the VPC endpoint service belongs.' + ), + ) + parser.add_argument( + '--server-type', + metavar='{LB, VM, VIP, BMS}', + type=lambda s: s.upper(), + choices=['LB', 'VM', 'VIP', 'BMS'], + required=True, + help=_('Specifies the resource type.'), + ) + parser.add_argument( + '--service-type', + metavar='{gateway, interface}', + type=lambda s: s.lower(), + choices=['gateway', 'interface'], + help=_('Specifies the type of the VPC endpoint service.'), + ) + parser.add_argument( + '--ports', + metavar='client_port=,' + 'server_port=,' + 'protocol=', + action=parseractions.MultiKeyValueAction, + required=True, + required_keys=['client_port', 'server_port', 'protocol'], + help=_( + 'Example: \n' + '--ports client_port=8081,server_port=22,protocol=TCP\n' + 'Repeat option to provide multiple ports.' + ), + ) + parser.add_argument( + '--tcp-proxy', + metavar='{close, toa_open, proxy_open, open, proxy_vni}', + type=lambda s: s.lower(), + choices=['close', 'toa_open', 'proxy_open', 'open', 'proxy_vni'], + help=_( + 'Whether the client IP address and port number or marker_id ' + 'information is transmitted to the server.' + ), + ) + parser.add_argument( + '--tags', + metavar='key=,value=', + action=parseractions.MultiKeyValueAction, + dest='tags', + required_keys=['key', 'value'], + help=_( + 'Example: \n' + '--tags key=test-key,value=test-value\n' + 'Repeat option to provide multiple tags.' + ), + ) + parser.add_argument( + '--disable-approval', + action='store_true', + help=_('Specifies whether connection approval is required.'), + ) + return parser + + @translate_response + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + + attrs = {'service_name': parsed_args.name} + args_list = [ + 'port_id', + 'pool_id', + 'vpc_id', + 'service_type', + 'server_type', + 'tags', + 'tcp_proxy', + ] + for arg in args_list: + val = getattr(parsed_args, arg) + if val: + attrs[arg] = val + ports = [] + for port in parsed_args.ports: + ports.append( + { + 'client_port': int(port['client_port']), + 'server_port': int(port['server_port']), + 'protocol': port['protocol'], + } + ) + attrs['ports'] = ports + + if parsed_args.disable_approval: + attrs['approval_enabled'] = False + return client.create_service(**attrs) + + +class UpdateService(command.ShowOne): + _description = _('Update a Endpoint Service.') + + def get_parser(self, prog_name): + parser = super(UpdateService, self).get_parser(prog_name) + parser.add_argument( + 'service', + metavar='', + help=_('Name or ID of the Vpc endpoint service.'), + ) + parser.add_argument( + '--name', + metavar='', + dest='service_name', + help=_('Name of the VPC endpoint service.'), + ) + parser.add_argument( + '--ports', + metavar='client_port=,' + 'server_port=,' + 'protocol=', + action=parseractions.MultiKeyValueAction, + required_keys=['client_port', 'server_port', 'protocol'], + help=_( + 'Example: \n' + '--ports client_port=8081,server_port=22,protocol=TCP\n' + 'Repeat option to provide multiple ports.' + ), + ) + parser.add_argument( + '--port-id', + metavar='', + help=_( + 'Specify the ID for identifying the backend resource of ' + 'the VPC endpoint service.' + ), + ) + parser.add_argument( + '--tcp-proxy', + metavar='{close, toa_open, proxy_open, open, proxy_vni}', + type=lambda s: s.lower(), + choices=['close', 'toa_open', 'proxy_open', 'open', 'proxy_vni'], + help=_( + 'Whether the client IP address and port number or marker_id ' + 'information is transmitted to the server.' + ), + ) + approval_group = parser.add_mutually_exclusive_group() + approval_group.add_argument( + '--enable-approval', + action='store_true', + help=_('Connection approval is required.'), + ) + approval_group.add_argument( + '--disable-approval', + action='store_true', + help=_('Connection approval is not required.'), + ) + return parser + + @translate_response + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + args_list = [ + 'service_name', + 'port_id', + 'tcp_proxy', + ] + attrs = {} + for arg in args_list: + val = getattr(parsed_args, arg) + if val: + attrs[arg] = val + ports = [] + for port in parsed_args.ports: + ports.append( + { + 'client_port': int(port['client_port']), + 'server_port': int(port['server_port']), + 'protocol': port['protocol'], + } + ) + attrs['ports'] = ports + if parsed_args.enable_approval: + attrs['approval_enabled'] = True + if parsed_args.disable_approval: + attrs['approval_enabled'] = False + service = client.find_service(parsed_args.service) + return client.update_service(service, **attrs) + + +class DeleteService(command.Command): + + _description = _('Deletes VPC Endpoint Service.') + + def get_parser(self, prog_name): + parser = super(DeleteService, self).get_parser(prog_name) + parser.add_argument( + 'service', + metavar='', + nargs='+', + help=_('Vpc Endpoint Services(s) to delete (Name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + result = 0 + for service in parsed_args.service: + try: + obj = client.find_service(service) + client.delete_service(obj.id) + except Exception as e: + result += 1 + LOG.error( + _( + 'Failed to delete Vpc Endpoint Service with ' + 'name or ID "%(service)s": %(e)s' + ), + {'service': service, 'e': e}, + ) + if result > 0: + total = len(parsed_args.service) + msg = _( + '%(result)s of %(total)s Vpc Endpoint Services(s) failed ' + 'to delete.' + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) diff --git a/otcextensions/osclient/vpcep/v1/whitelist.py b/otcextensions/osclient/vpcep/v1/whitelist.py new file mode 100644 index 000000000..b48549014 --- /dev/null +++ b/otcextensions/osclient/vpcep/v1/whitelist.py @@ -0,0 +1,157 @@ +# 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. +# +"""VPC Endpoint Service v1 action implementations""" +import logging + +from osc_lib import utils +from osc_lib.command import command + +from otcextensions.common import sdk_utils +from otcextensions.i18n import _ + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = {} + hidden = [ + 'location', + ] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden + ) + + +class ListWhitelist(command.Lister): + + _description = _('List whitelist records of a VPC endpoint service.') + columns = ('Id', 'Permission', 'Created At') + + def get_parser(self, prog_name): + parser = super(ListWhitelist, self).get_parser(prog_name) + + parser.add_argument( + 'service', + metavar='', + help=_('ID or name of the VPC Endpoint Service.'), + ) + parser.add_argument( + '--sort-key', + metavar='{created_at}', + type=lambda s: s.lower(), + choices=['created_at'], + help=_('Sorting field of the whitelist records.'), + ) + parser.add_argument( + '--sort-dir', + metavar='{asc, desc}', + type=lambda s: s.lower(), + choices=['asc', 'desc'], + help=_('Sorting order of the whitelist record list.'), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Limit number of whitelist records to fetch.'), + ) + parser.add_argument( + '--offset', + metavar='', + type=int, + help=_('Whitelist records after this Offset will be queried.'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + args_list = [ + 'limit', + 'offset', + 'sort_key', + 'sort_dir', + ] + attrs = {} + for arg in args_list: + val = getattr(parsed_args, arg) + if val: + attrs[arg] = val + endpoint_service = client.find_service(parsed_args.service) + data = client.service_whitelist(endpoint_service, **attrs) + + return ( + self.columns, + (utils.get_item_properties(s, self.columns) for s in data), + ) + + +class ManageWhitelist(command.Lister): + _description = _('Manage whitelist records of a VPC endpoint service.') + + columns = ('Permission',) + + def get_parser(self, prog_name): + parser = super(ManageWhitelist, self).get_parser(prog_name) + + parser.add_argument( + 'service', + metavar='', + help=_('ID or name of the VPC Endpoint Service.'), + ) + parser.add_argument( + 'domain', + metavar='', + nargs='+', + help=_( + 'Domain ID(s) to add to whitelist record of the ' + 'Vpc endpoint service.' + ), + ) + manage_request_group = parser.add_mutually_exclusive_group( + required=True + ) + manage_request_group.add_argument( + '--add', + action='store_true', + help=( + 'Add a domian to the whitelist record of the ' + 'Vpc endpoint service.' + ), + ) + manage_request_group.add_argument( + '--remove', + action='store_true', + help=( + 'Remove a domian from the whitelist record of the ' + 'Vpc endpoint service.' + ), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.vpcep + set_args = ('add', 'remove') + request_status = [ + request for request in set_args if getattr(parsed_args, request) + ] + + endpoint_service = client.find_service(parsed_args.service) + data = client.manage_service_whitelist( + endpoint_service, + domains=parsed_args.domain, + action=request_status[0], + ) + return ( + self.columns, + (utils.get_item_properties(s, self.columns) for s in data), + ) diff --git a/otcextensions/sdk/vpcep/v1/_proxy.py b/otcextensions/sdk/vpcep/v1/_proxy.py index 38cfb42e8..078eac482 100644 --- a/otcextensions/sdk/vpcep/v1/_proxy.py +++ b/otcextensions/sdk/vpcep/v1/_proxy.py @@ -9,9 +9,275 @@ # 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 exceptions from openstack import proxy +from otcextensions.sdk.vpcep.v1 import connection as _connection +from otcextensions.sdk.vpcep.v1 import endpoint as _endpoint +from otcextensions.sdk.vpcep.v1 import quota as _quota +from otcextensions.sdk.vpcep.v1 import service as _service +from otcextensions.sdk.vpcep.v1 import whitelist as _whitelist class Proxy(proxy.Proxy): - skip_discovery = True + + # ======== VPC Endpoint ======== + + def endpoints(self, **query): + """Return a generator of endpoints + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + :returns: A generator of endpoint objects + :rtype: :class:`~otcextensions.sdk.vpcep.endpoint.Endpoint` + """ + if query.get('limit'): + query.update(paginated=False) + return self._list(_endpoint.Endpoint, **query) + + def create_endpoint(self, **attrs): + """Create a new endpoint from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~otcextensions.sdk.vpcep.endpoint.Endpoint`, + comprised of the properties on the Endpoint class. + + :returns: The results of endpoint creation + :rtype: :class:`~otcextensions.sdk.vpcep.endpoint.Endpoint` + """ + return self._create(_endpoint.Endpoint, **attrs) + + def get_endpoint(self, endpoint): + """Get a single endpoint + + :param endpoint: The value can be the ID of a endpoint or a + :class:`~otcextensions.sdk.vpcep.endpoint.Endpoint` + instance. + + :returns: One :class:`~otcextensions.sdk.vpcep.endpoint.Endpoint` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + + """ + return self._get(_endpoint.Endpoint, endpoint) + + def delete_endpoint(self, endpoint, ignore_missing=True): + """Delete a endpoint + + :param endpoint: The value can be either the ID of a endpoint or a + :class:`~otcextensions.sdk.vpcep.endpoint.Endpoint` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the endpoint does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent endpoint. + + :returns: ``None`` + """ + return self._delete( + _endpoint.Endpoint, endpoint, ignore_missing=ignore_missing + ) + + # ======== Endpoint Service ======== + + def services(self, **query): + """Return a generator of endpoint services + + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + :returns: A generator of endpoint service objects + :rtype: :class:`~otcextensions.sdk.vpcep.service.Service` + """ + if query.get('limit'): + query.update(paginated=False) + return self._list(_service.Service, **query) + + def create_service(self, **attrs): + """Create a new endpoint service from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~otcextensions.sdk.vpcep.service.Service`, + comprised of the properties on the Service class. + + :returns: The results of endpoint service creation + :rtype: :class:`~otcextensions.sdk.vpcep.service.Service` + """ + return self._create(_service.Service, **attrs) + + def get_service(self, service): + """Get a single endpoint service + + :param service: The value can be the ID of a endpoint service or a + :class:`~otcextensions.sdk.vpcep.service.Service` + instance. + + :returns: One :class:`~otcextensions.sdk.vpcep.service.Service` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + + """ + return self._get(_service.Service, service) + + def find_service(self, name_or_id, ignore_missing=False): + """Find a single endpoint service + + :param name_or_id: The name or ID of a service. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + + :returns: One :class:`~otcextensions.sdk.vpcep.service.Service` + or None + """ + return self._find( + _service.Service, + name_or_id, + ignore_missing=ignore_missing, + ) + + def update_service(self, service, **attrs): + """Update a endpoint service + + :param service: The service can be either the ID or a + :class:`~otcextensions.sdk.vpcep.service.Service` instance. + :param dict attrs: The attributes to update on the endpoint service + represented by ``service``. + + :returns: The updated service + :rtype: :class:`~otcextensions.sdk.vpcep.service.Service` + """ + return self._update(_service.Service, service, **attrs) + + def delete_service(self, service, ignore_missing=True): + """Delete a endpoint service + + :param service: The service can be either the ID or a + :class:`~otcextensions.sdk.vpcep.service.Service` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the service does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent service. + + :returns: ``None`` + """ + return self._delete( + _service.Service, + service, + ignore_missing=ignore_missing, + ) + + # ======== Endpoint Service Connections ======== + + def service_connections(self, service, **query): + """Return a generator of service connections + + :param service: The service can be either the ID or a + :class:`~otcextensions.sdk.vpcep.service.Service` instance. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + :returns: A generator of connection objects. + :rtype: :class:`~otcextensions.sdk.vpcep.connection.Connection` + """ + if query.get('limit'): + query.update(paginated=False) + endpoint_service = self._get_resource(_service.Service, service) + return self._list( + _connection.Connection, + endpoint_service_id=endpoint_service.id, + **query + ) + + def manage_service_connections(self, service, action, endpoints=[]): + """Manage endpoint service connections + + :param service: The service can be either the ID or a + :class:`~otcextensions.sdk.vpcep.service.Service` instance. + :param action: action can be ``accept`` or ``reject``. + :param endpoints: List of VPC Endpoints Id. + + :returns: A generator of connection objects. + :rtype: :class:`~otcextensions.sdk.vpcep.connection.Connection` + """ + + endpoint_service = self._get_resource(_service.Service, service) + connection = self._get_resource( + _connection.Connection, + None, + endpoint_service_id=endpoint_service.id, + ) + if action in ('accept', 'receive'): + return connection.accept(self, endpoints) + elif action == 'reject': + return connection.reject(self, endpoints) + raise exceptions.SDKException( + "Value of action can be 'accept' or 'reject'." + ) + + # ======== Endpoint Service Whitelist ======== + + def service_whitelist(self, service, **query): + """Return a generator of service whitelist + + :param service: The service can be either the ID or a + :class:`~otcextensions.sdk.vpcep.service.Service` instance. + :param dict query: Optional query parameters to be sent to limit + the resources being returned. Valid parameters are: + + :returns: A generator of whitelist objects. + :rtype: :class:`~otcextensions.sdk.vpcep.whitelist.Whitelist` + """ + if query.get('limit'): + query.update(paginated=False) + endpoint_service = self._get_resource(_service.Service, service) + return self._list( + _whitelist.Whitelist, + endpoint_service_id=endpoint_service.id, + **query + ) + + def manage_service_whitelist(self, service, action, domains=[]): + """Manage endpoint service whitelist + + :param service: The service can be either the ID or a + :class:`~otcextensions.sdk.vpcep.service.Service` instance. + :param action: action can be ``add`` or ``remove``. + :param domains: List of domains Ids to be added to whitelist. + + :returns: A generator of whitelist objects. + :rtype: :class:`~otcextensions.sdk.vpcep.whitelist.Whitelist` + """ + endpoint_service = self._get_resource(_service.Service, service) + whitelist = self._get_resource( + _whitelist.Whitelist, + None, + endpoint_service_id=endpoint_service.id, + ) + if action == 'add': + return whitelist.add(self, domains) + elif action == 'remove': + return whitelist.remove(self, domains) + raise exceptions.SDKException( + "Value of action can be 'add' or 'remove'." + ) + + # ======== VPCEP Resource Quota ======== + + def resource_quota(self, type=None): + """Return a generator of vpcep quota + + :param type: Specify the resource type. Value of + type can be: ``endpoint`` or ``endpoint_service`` + + :returns: A generator of quota objects. + :rtype: :class:`~otcextensions.sdk.vpcep.quota.Quota` + """ + query = {} + if type: + query.update(type=type) + return self._list(_quota.Quota, **query) diff --git a/otcextensions/sdk/vpcep/v1/connection.py b/otcextensions/sdk/vpcep/v1/connection.py new file mode 100644 index 000000000..6146853f8 --- /dev/null +++ b/otcextensions/sdk/vpcep/v1/connection.py @@ -0,0 +1,66 @@ +# 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 exceptions +from openstack import resource +from openstack import utils + + +class Connection(resource.Resource): + base_path = '/vpc-endpoint-services/%(endpoint_service_id)s/connections' + resources_key = 'connections' + + # capabilities + allow_list = True + + _query_mapping = resource.QueryParameters( + 'id', + 'limit', + 'marker_id', + 'offset', + 'sort_dir', + 'sort_key', + ) + + # URI properties + endpoint_service_id = resource.URI('endpoint_service_id') + + # Properties + #: Creation time of the VPC endpoint. + created_at = resource.Body('created_at') + #: User's domain ID. + domain_id = resource.Body('domain_id') + #: ID of the VPC endpoint. + id = resource.Body('id') + #: Packet ID of the VPC endpoint. + marker_id = resource.Body('marker_id', type=int) + #: Connection status of the VPC endpoint. + status = resource.Body('status') + #: Update time of the VPC endpoint. + updated_at = resource.Body('updated_at') + + def _action(self, session, action, endpoints=[]): + """Preform actions given the message body.""" + uri = utils.urljoin(self.base_path % self._uri.attributes, 'action') + body = {'endpoints': endpoints, 'action': action} + response = session.post(uri, json=body) + exceptions.raise_from_response(response) + for raw_resource in response.json()[self.resources_key]: + yield Connection.existing(**raw_resource) + + def accept(self, session, endpoints=[]): + """Accept connections.""" + return self._action(session, 'receive', endpoints) + + def reject(self, session, endpoints=[]): + """Reject Connections.""" + return self._action(session, 'reject', endpoints) diff --git a/otcextensions/sdk/vpcep/v1/endpoint.py b/otcextensions/sdk/vpcep/v1/endpoint.py new file mode 100644 index 000000000..7fc4c4547 --- /dev/null +++ b/otcextensions/sdk/vpcep/v1/endpoint.py @@ -0,0 +1,179 @@ +# 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 exceptions +from openstack import resource + + +class TagsSpec(resource.Resource): + #: Specifies the tag key. + key = resource.Body('key') + #: Specifies the tag value. + value = resource.Body('value') + + +class Endpoint(resource.Resource): + base_path = '/vpc-endpoints' + resources_key = 'endpoints' + resource_key = None + + # capabilities + allow_create = True + allow_fetch = True + allow_delete = True + allow_list = True + + _query_mapping = resource.QueryParameters( + 'id', + 'offset', + 'limit', + 'router_id', + 'endpoint_service_name', + 'sort_dir', + 'sort_key', + router_id='vpc_id', + ) + + # Properties + #: Domain status. + active_status = resource.Body('active_status', type=list) + #: Creation time of the VPC endpoint. + created_at = resource.Body('created_at') + #: Description of the VPC endpoint. + description = resource.Body('description') + #: Domain name for accessing the associated VPC endpoint service. + dns_names = resource.Body('dns_names', type=list) + #: ID of the cluster associated with the VPC endpoint. + endpoint_pool_id = resource.Body('endpoint_pool_id') + #: ID of the VPC endpoint service. + endpoint_service_id = resource.Body('endpoint_service_id') + #: Name of the VPC endpoint service. + endpoint_service_name = resource.Body('endpoint_service_name') + #: VPC endpoint ID. + id = resource.Body('id') + #: IP address for accessing the associated VPC endpoint service. + ip = resource.Body('ip') + #: Whether to create a private domain name. + is_dns_enabled = resource.Body('enable_dns', type=bool) + #: Whether the VPC endpoint is enabled. + is_enabled = resource.Body('enable_status', type=bool) + #: Whether access control is enabled. + is_whitelist_enabled = resource.Body('enable_whitelist', type=bool) + #: Packet ID of the VPC endpoint. + marker_id = resource.Body('marker_id', type=int) + #: Specifies the ID of the network in the VPC/Router. + network_id = resource.Body('subnet_id') + #: Project ID. + project_id = resource.Body('project_id') + #: IDs of route tables. + route_tables = resource.Body('routetables', type=list) + #: ID of the VPC/Router where the VPC endpoint is to be created. + router_id = resource.Body('vpc_id') + #: Type of the VPC endpoint service that is associated with the + #: VPC endpoint. + service_type = resource.Body('service_type') + #: Specifies the name of the VPC endpoint specifications. + specification_name = resource.Body('specification_name') + #: Specifies the connection status of the VPC endpoint. + status = resource.Body('status') + #: Lists the resource tags. + tags = resource.Body('tags', type=list, list_type=TagsSpec) + #: Update time of the VPC endpoint. + updated_at = resource.Body('updated_at') + #: Whitelist for controlling access to the VPC endpoint. + whitelist = resource.Body('whitelist', type=list) + + @classmethod + def existing(cls, connection=None, **kwargs): + """Create an instance of an existing remote resource. + + When creating the instance set the ``_synchronized`` parameter + of :class:`Resource` to ``True`` to indicate that it represents the + state of an existing server-side resource. As such, all attributes + passed in ``**kwargs`` are considered "clean", such that an immediate + :meth:`update` call would not generate a body of attributes to be + modified on the server. + + :param dict kwargs: Each of the named arguments will be set as + attributes on the resulting Resource object. + """ + if 'enable_status' in kwargs.keys(): + enable_status = kwargs['enable_status'] + kwargs['enable_status'] = ( + True if enable_status == 'enable' else False + ) + return cls(_synchronized=True, connection=connection, **kwargs) + + def _translate_response( + self, + response, + has_body=None, + error_message=None, + *, + resource_response_key=None, + ): + """Given a KSA response, inflate this instance with its data + + DELETE operations don't return a body, so only try to work + with a body when has_body is True. + + This method updates attributes that correspond to headers + and body on this instance and clears the dirty set. + """ + if has_body is None: + has_body = self.has_body + + exceptions.raise_from_response(response, error_message=error_message) + + if has_body: + try: + body = response.json() + if resource_response_key and resource_response_key in body: + body = body[resource_response_key] + elif self.resource_key and self.resource_key in body: + body = body[self.resource_key] + if 'enable_status' in body.keys(): + enable_status = body['enable_status'] + body['enable_status'] = ( + True if enable_status == 'enable' else False + ) + # Do not allow keys called "self" through. Glance chose + # to name a key "self", so we need to pop it out because + # we can't send it through cls.existing and into the + # Resource initializer. "self" is already the first + # argument and is practically a reserved word. + body.pop("self", None) + + body_attrs = self._consume_body_attrs(body) + if self._allow_unknown_attrs_in_body: + body_attrs.update(body) + self._unknown_attrs_in_body.update(body) + elif self._store_unknown_attrs_as_properties: + body_attrs = self._pack_attrs_under_properties( + body_attrs, body + ) + + self._body.attributes.update(body_attrs) + self._body.clean() + if self.commit_jsonpatch or self.allow_patch: + # We need the original body to compare against + self._original_body = body_attrs.copy() + except ValueError: + # Server returned not parse-able response (202, 204, etc) + # Do simply nothing + pass + + headers = self._consume_header_attrs(response.headers) + self._header.attributes.update(headers) + self._header.clean() + self._update_location() + dict.update(self, self.to_dict()) diff --git a/otcextensions/sdk/vpcep/v1/quota.py b/otcextensions/sdk/vpcep/v1/quota.py new file mode 100644 index 000000000..904b96c37 --- /dev/null +++ b/otcextensions/sdk/vpcep/v1/quota.py @@ -0,0 +1,113 @@ +# 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 exceptions +from openstack import resource + + +class Quota(resource.Resource): + base_path = '/quotas' + + # capabilities + allow_list = True + + _query_mapping = resource.QueryParameters( + 'type', + ) + + # Properties + #: Specifies the maximum quota of resources. + quota = resource.Body('quota', type=int) + #: Specifies the resource type. + type = resource.Body('type') + #: Specifies the number of created resources. + used = resource.Body('used', type=int) + + @classmethod + def list(cls, session, paginated=False, base_path=None, **params): + """This method is a generator which yields resource objects. + + This resource object list generator handles pagination and takes query + params for response filtering. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param bool paginated: ``True`` if a GET to this resource returns + a paginated series of responses, or ``False`` if a GET returns only + one page of data. **When paginated is False only one page of data + will be returned regardless of the API's support of pagination.** + :param str base_path: Base part of the URI for listing resources, if + different from :data:`~openstack.resource.Resource.base_path`. + :param bool allow_unknown_params: ``True`` to accept, but discard + unknown query parameters. This allows getting list of 'filters' and + passing everything known to the server. ``False`` will result in + validation exception when unknown query parameters are passed. + :param str microversion: API version to override the negotiated one. + :param dict params: These keyword arguments are passed through the + :meth:`~openstack.resource.QueryParamter._transpose` method + to find if any of them match expected query parameters to be sent + in the *params* argument to + :meth:`~keystoneauth1.adapter.Adapter.get`. They are additionally + checked against the :data:`~openstack.resource.Resource.base_path` + format string to see if any path fragments need to be filled in by + the contents of this argument. + Parameters supported as filters by the server side are passed in + the API call, remaining parameters are applied as filters to the + retrieved results. + + :return: A generator of :class:`Resource` objects. + :raises: :exc:`~openstack.exceptions.MethodNotSupported` if + :data:`Resource.allow_list` is not set to ``True``. + :raises: :exc:`~openstack.exceptions.InvalidResourceQuery` if query + contains invalid params. + """ + + if not cls.allow_list: + raise exceptions.MethodNotSupported(cls, "list") + session = cls._get_session(session) + microversion = cls._get_microversion(session, action='list') + + if base_path is None: + base_path = cls.base_path + cls._query_mapping._validate(params, base_path=base_path) + query_params = cls._query_mapping._transpose(params, cls) + uri = base_path % params + + # Copy query_params due to weird mock unittest interactions + response = session.get( + uri, + headers={"Accept": "application/json"}, + params=query_params.copy(), + microversion=microversion, + ) + exceptions.raise_from_response(response) + data = response.json() + + quotas_data = data.get('quotas') + if not quotas_data: + return + + resources = quotas_data.get('resources') + if not resources: + return + + if not isinstance(resources, list): + resources = [resources] + + for raw_resource in resources: + raw_resource.pop("self", None) + value = cls.existing( + microversion=microversion, + connection=session._get_connection(), + **raw_resource + ) + yield value diff --git a/otcextensions/sdk/vpcep/v1/service.py b/otcextensions/sdk/vpcep/v1/service.py new file mode 100644 index 000000000..7d2f436a2 --- /dev/null +++ b/otcextensions/sdk/vpcep/v1/service.py @@ -0,0 +1,113 @@ +# 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 exceptions +from openstack import resource + + +class TagsSpec(resource.Resource): + #: Tag key. + key = resource.Body('key') + #: Tag value. + value = resource.Body('value') + + +class PortSpec(resource.Resource): + #: Port for accessing the VPC endpoint. + client_port = resource.Body('client_port', type=int) + #: Port mapping protocol. + protocol = resource.Body('protocol') + #: Port for accessing the VPC endpoint service. + server_port = resource.Body('server_port', type=int) + + +class Service(resource.Resource): + base_path = '/vpc-endpoint-services' + resources_key = 'endpoint_services' + + _query_mapping = resource.QueryParameters( + 'id', + 'limit', + 'name', + 'offset', + 'sort_key', + 'sort_dir', + 'status', + name='endpoint_service_name', + ) + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + + # Properties + #: Creation time of the VPC endpoint service. + created_at = resource.Body('created_at') + #: Count of Creating or Accepted VPC endpoints under the + #: VPC endpoint service. + connection_count = resource.Body('connection_count', type=int) + #: Supplementary information about the VPC endpoint service. + description = resource.Body('description') + #: Specifies the user's domain ID. + domain_id = resource.Body('domain_id') + #: ID of the VPC endpoint service. + id = resource.Body('id') + #: Whether connection approval is required. + is_approval_enabled = resource.Body('approval_enabled', type=bool) + #: ID of the cluster associated with the target VPCEP resource. + pool_id = resource.Body('pool_id') + #: ID for identifying the backend resource of the VPC endpoint service. + port_id = resource.Body('port_id') + #: Lists the port mappings opened to the VPC endpoint service. + ports = resource.Body('ports', type=list, list_type=PortSpec) + #: Project ID. + project_id = resource.Body('project_id') + #: ID of the router to which the backend resource of the VPC endpoint + #: service belongs. + router_id = resource.Body('vpc_id') + #: Resource type. + server_type = resource.Body('server_type') + #: Name of the VPC endpoint service. + service_name = resource.Body('service_name') + #: Type of the VPC endpoint service. + service_type = resource.Body('service_type') + #: Status of the VPC endpoint service. + status = resource.Body('status') + #: Lists the resource tags. + tags = resource.Body('tags', type=list, list_type=TagsSpec) + #: Specifies whether the client IP address and port number or marker_id + #: information is transmitted to the server. + tcp_proxy = resource.Body('tcp_proxy') + #: Update time of the VPC endpoint service. + updated_at = resource.Body('updated_at') + + @classmethod + def _get_one_match(cls, name_or_id, results): + """Given a list of results, return the match""" + the_result = None + for maybe_result in results: + id_value = cls._get_id(maybe_result) + name_value = maybe_result.service_name + + if (id_value == name_or_id) or (name_value == name_or_id): + # Only allow one resource to be found. If we already + # found a match, raise an exception to show it. + if the_result is None: + the_result = maybe_result + else: + msg = "More than one %s exists with the name '%s'." + msg = msg % (cls.__name__, name_or_id) + raise exceptions.DuplicateResource(msg) + return the_result diff --git a/otcextensions/sdk/vpcep/v1/whitelist.py b/otcextensions/sdk/vpcep/v1/whitelist.py new file mode 100644 index 000000000..55982f763 --- /dev/null +++ b/otcextensions/sdk/vpcep/v1/whitelist.py @@ -0,0 +1,62 @@ +# 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 exceptions +from openstack import resource +from openstack import utils + + +class Whitelist(resource.Resource): + base_path = '/vpc-endpoint-services/%(endpoint_service_id)s/permissions' + resources_key = 'permissions' + + # capabilities + allow_list = True + + _query_mapping = resource.QueryParameters( + 'permission', 'sort_key', 'sort_dir', 'limit', 'offset' + ) + + # URI properties + endpoint_service_id = resource.URI('endpoint_service_id') + + # Properties + #: Specifies when the whitelist record is added. + created_at = resource.Body('created_at') + #: Specifies the description of a whitelist record of a VPC endpoint + #: service. + description = resource.Body('description') + #: Specifies the unique ID of the permission. + id = resource.Body('id') + #: Lists the whitelist records. + permission = resource.Body('permission') + + def _action(self, session, action, domains=[]): + """Preform actions on the request body.""" + uri = utils.urljoin(self.base_path % self._uri.attributes, 'action') + for ix, domain in enumerate(domains): + if not domain.startswith('iam:domain::'): + domains[ix] = 'iam:domain::' + domain + + body = {'permissions': domains, 'action': action} + response = session.post(uri, json=body) + exceptions.raise_from_response(response) + for raw_resource in response.json()[self.resources_key]: + yield Whitelist.existing(permission=raw_resource) + + def add(self, session, domains=[]): + """Add whitelist.""" + return self._action(session, 'add', domains) + + def remove(self, session, domains=[]): + """Remove whitelist.""" + return self._action(session, 'remove', domains) diff --git a/otcextensions/sdk/vpcep/vpcep_service.py b/otcextensions/sdk/vpcep/vpcep_service.py index 754412769..40894d56b 100644 --- a/otcextensions/sdk/vpcep/vpcep_service.py +++ b/otcextensions/sdk/vpcep/vpcep_service.py @@ -9,14 +9,13 @@ # 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 service_description -from otcextensions.sdk.lts.v2 import _proxy +from otcextensions.sdk.vpcep.v1 import _proxy class VpcepService(service_description.ServiceDescription): - """The Vpcep service.""" + """The VPCEP service.""" - supported_versions = { - '1': _proxy.Proxy - } + supported_versions = {'1': _proxy.Proxy} diff --git a/otcextensions/tests/unit/osclient/vpcep/__init__.py b/otcextensions/tests/unit/osclient/vpcep/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/unit/osclient/vpcep/v1/__init__.py b/otcextensions/tests/unit/osclient/vpcep/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/unit/osclient/vpcep/v1/fakes.py b/otcextensions/tests/unit/osclient/vpcep/v1/fakes.py new file mode 100644 index 000000000..7e7dcea5c --- /dev/null +++ b/otcextensions/tests/unit/osclient/vpcep/v1/fakes.py @@ -0,0 +1,161 @@ +# 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 +from datetime import datetime + +import mock +from openstackclient.tests.unit import utils +from osc_lib import utils as _osc_lib_utils + +from otcextensions.sdk.vpcep.v1 import connection +from otcextensions.sdk.vpcep.v1 import endpoint +from otcextensions.sdk.vpcep.v1 import quota +from otcextensions.sdk.vpcep.v1 import service +from otcextensions.sdk.vpcep.v1 import whitelist +from otcextensions.tests.unit.osclient import test_base + + +def gen_data(obj, columns, formatters=None): + """Fill expected data tuple based on columns list""" + return _osc_lib_utils.get_item_properties( + obj, columns, formatters=formatters + ) + + +def gen_data_dict(data, columns): + """Fill expected data tuple based on columns list""" + return tuple(data.get(attr, '') for attr in columns) + + +class TestVpcep(utils.TestCommand): + def setUp(self): + super(TestVpcep, self).setUp() + + self.app.client_manager.vpcep = mock.Mock() + + self.client = self.app.client_manager.vpcep + + +class FakeService(test_base.Fake): + """Fake one or more endpoint service(s).""" + + @classmethod + def generate(cls): + """Create a fake endpoint service. + :return: + A FakeResource object, with id, name and so on + """ + # Set default attributes. + object_info = { + 'id': 'id-' + uuid.uuid4().hex, + 'port_id': 'port-id-' + uuid.uuid4().hex, + 'vpc_id': 'vpc-id-' + uuid.uuid4().hex, + 'pool_id': 'pool-id-' + uuid.uuid4().hex, + 'status': 'available', + 'approval_enabled': False, + 'service_name': 'vpcep-service-' + uuid.uuid4().hex, + 'service_type': 'interface', + 'server_type': 'VM', + 'project_id': 'project-id-' + uuid.uuid4().hex, + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f'), + 'ports': [ + {'client_port': 8080, 'server_port': 90, 'protocol': 'TCP'}, + {'client_port': 8081, 'server_port': 80, 'protocol': 'TCP'}, + ], + } + + return service.Service(**object_info) + + +class FakeEndpoint(test_base.Fake): + """Fake one or more Endpoint(s).""" + + @classmethod + def generate(cls): + """Create a fake endpoint. + :return: + A FakeResource object, with id, status and so on + """ + # Set default attributes. + object_info = { + 'id': 'id-' + uuid.uuid4().hex, + 'service_type': 'interface', + 'marker_id': 322312312312, + 'status': 'creating', + 'vpc_id': 'vpc-id-' + uuid.uuid4().hex, + 'enable_dns': False, + 'is_enabled': True, + 'endpoint_service_name': 'vpcep-' + uuid.uuid4().hex, + 'endpoint_service_id': 'ep-service-id-' + uuid.uuid4().hex, + 'project_id': 'project-id-' + uuid.uuid4().hex, + 'whitelist': ['127.0.0.1'], + 'enable_whitelist': True, + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f'), + 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f'), + 'tags': [{'key': 'test1', 'value': 'test1'}], + } + + return endpoint.Endpoint(**object_info) + + +class FakeWhitelist(test_base.Fake): + """Fake one or more Whitelist.""" + + @classmethod + def generate(cls): + """Create a fake service whitelist. + :return: + A FakeResource object, with id, status and so on + """ + # Set default attributes. + object_info = { + 'id': 'id-' + uuid.uuid4().hex, + 'permission': '*', + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f'), + } + return whitelist.Whitelist(**object_info) + + +class FakeConnection(test_base.Fake): + """Fake one or more Connection(s).""" + + @classmethod + def generate(cls): + """Create a fake service connection. + :return: + A FakeResource object, with id, status and so on + """ + # Set default attributes. + object_info = { + 'id': 'id-' + uuid.uuid4().hex, + 'status': 'accepted', + 'marker_id': 16777510, + 'domain_id': '5fc973eea581490997e82ea11a1df31f', + 'created_at': '2018-09-17T11:10:11Z', + 'updated_at': '2018-09-17T11:10:12Z', + } + return connection.Connection(**object_info) + + +class FakeQuota(test_base.Fake): + """Fake one or more resource Quota(s).""" + + @classmethod + def generate(cls): + """Create a fake resource quota. + :return: + A FakeResource object, with id, status and so on + """ + # Set default attributes. + object_info = {'type': 'endpoint', 'quota': 50, 'used': 4} + return quota.Quota(**object_info) diff --git a/otcextensions/tests/unit/osclient/vpcep/v1/test_connection.py b/otcextensions/tests/unit/osclient/vpcep/v1/test_connection.py new file mode 100644 index 000000000..ffe18492c --- /dev/null +++ b/otcextensions/tests/unit/osclient/vpcep/v1/test_connection.py @@ -0,0 +1,187 @@ +# 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.vpcep.v1 import connection +from otcextensions.tests.unit.osclient.vpcep.v1 import fakes + + +class TestListWhitelist(fakes.TestVpcep): + + _service = fakes.FakeService.create_one() + objects = fakes.FakeConnection.create_multiple(3) + column_list_headers = ( + 'Id', + 'Domain Id', + 'Status', + 'Created At', + 'Updated At', + ) + + columns = ( + 'id', + 'domain_id', + 'status', + 'created_at', + 'updated_at', + ) + + data = [] + + for s in objects: + data.append( + ( + s.id, + s.domain_id, + s.status, + s.created_at, + s.updated_at, + ) + ) + + def setUp(self): + super(TestListWhitelist, self).setUp() + + self.cmd = connection.ListConnections(self.app, None) + + self.client.find_service = mock.Mock(return_value=self._service) + self.client.service_connections = mock.Mock() + self.client.api_mock = self.client.service_connections + + def test_list(self): + arglist = [self._service.name] + + verifylist = [ + ('service', self._service.name), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with(self._service) + + self.assertEqual(self.column_list_headers, columns) + self.assertEqual(self.data, list(data)) + + def test_list_args(self): + arglist = [ + self._service.name, + '--sort-key', + 'created_at', + '--sort-dir', + 'asc', + '--limit', + '2', + '--offset', + '3', + ] + + verifylist = [ + ('service', self._service.name), + ('sort_key', 'created_at'), + ('sort_dir', 'asc'), + ('limit', 2), + ('offset', 3), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with( + self._service, + sort_key='created_at', + sort_dir='asc', + limit=2, + offset=3, + ) + + +class TestManageConnections(fakes.TestVpcep): + + _service = fakes.FakeService.create_one() + objects = fakes.FakeConnection.create_multiple(3) + column_list_headers = ( + 'Id', + 'Domain Id', + 'Status', + 'Created At', + 'Updated At', + ) + + columns = ( + 'id', + 'domain_id', + 'status', + 'created_at', + 'updated_at', + ) + + data = [] + + for s in objects: + data.append( + ( + s.id, + s.domain_id, + s.status, + s.created_at, + s.updated_at, + ) + ) + + def setUp(self): + super(TestManageConnections, self).setUp() + + self.cmd = connection.ManageConnections(self.app, None) + + self.client.find_service = mock.Mock(return_value=self._service) + self.client.manage_service_connections = mock.Mock() + self.client.api_mock = self.client.manage_service_connections + + def test_accept_connection(self): + arglist = [self._service.name, '123', 'xyz', '--accept'] + + verifylist = [ + ('service', self._service.name), + ('endpoint', ['123', 'xyz']), + ('receive', True), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with( + self._service, + action='receive', + endpoints=['123', 'xyz'], + ) + + self.assertEqual(self.column_list_headers, columns) + self.assertEqual(self.data, list(data)) diff --git a/otcextensions/tests/unit/osclient/vpcep/v1/test_endpoint.py b/otcextensions/tests/unit/osclient/vpcep/v1/test_endpoint.py new file mode 100644 index 000000000..8a63fbbbb --- /dev/null +++ b/otcextensions/tests/unit/osclient/vpcep/v1/test_endpoint.py @@ -0,0 +1,379 @@ +# 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 unittest.mock import call + +import mock +from openstackclient.tests.unit import utils as tests_utils +from osc_lib import exceptions + +from otcextensions.osclient.vpcep.v1 import endpoint +from otcextensions.tests.unit.osclient.vpcep.v1 import fakes + + +class TestListEndpoints(fakes.TestVpcep): + + objects = fakes.FakeEndpoint.create_multiple(3) + + column_list_headers = ( + 'Id', + 'Endpoint Service Name', + 'Status', + 'Is Enabled', + ) + + columns = ( + 'id', + 'endpoint_service_name', + 'status', + 'is_enabled', + ) + + data = [] + + for s in objects: + data.append( + ( + s.id, + s.endpoint_service_name, + s.status, + s.is_enabled, + ) + ) + + def setUp(self): + super(TestListEndpoints, self).setUp() + + self.cmd = endpoint.ListEndpoints(self.app, None) + + self.client.endpoints = mock.Mock() + self.client.api_mock = self.client.endpoints + + def test_list(self): + arglist = [] + + verifylist = [] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + 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 = [ + '--id', + '1', + '--service-name', + '2', + '--sort-key', + 'created_at', + '--sort-dir', + 'desc', + '--limit', + '6', + '--offset', + '7', + ] + + verifylist = [ + ('id', '1'), + ('endpoint_service_name', '2'), + ('sort_key', 'created_at'), + ('sort_dir', 'desc'), + ('limit', 6), + ('offset', 7), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with( + id='1', + endpoint_service_name='2', + sort_key='created_at', + sort_dir='desc', + limit=6, + offset=7, + ) + + +class TestCreateEndpoint(fakes.TestVpcep): + + _data = fakes.FakeEndpoint.create_one() + + columns = ( + 'created_at', + 'endpoint_service_id', + 'endpoint_service_name', + 'id', + 'is_dns_enabled', + 'is_enabled', + 'is_whitelist_enabled', + 'marker_id', + 'project_id', + 'router_id', + 'service_type', + 'status', + 'tags', + 'updated_at', + 'whitelist', + ) + + data = fakes.gen_data(_data, columns, formatters=endpoint._formatters) + + def setUp(self): + super(TestCreateEndpoint, self).setUp() + + self.cmd = endpoint.CreateEndpoint(self.app, None) + self.client.create_endpoint = mock.Mock( + return_value=fakes.FakeEndpoint.create_one() + ) + + def test_create(self): + arglist = [ + '--service-id', + '1', + '--router-id', + '2', + '--network-id', + '3', + '--port-ip', + '4', + '--route-tables', + 'abc', + '123', + '--whitelist', + 'xyz', + '456', + '--specification-name', + '5', + '--description', + '6', + '--tags', + 'key=tag-key,value=tag-value', + '--enable-dns', + '--enable-whitelist', + ] + verifylist = [ + ('endpoint_service_id', '1'), + ('vpc_id', '2'), + ('subnet_id', '3'), + ('port_ip', '4'), + ('routetables', ['abc', '123']), + ('whitelist', ['xyz', '456']), + ('specification_name', '5'), + ('description', '6'), + ('tags', [{'key': 'tag-key', 'value': 'tag-value'}]), + ('enable_dns', True), + ('enable_whitelist', True), + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + attrs = { + 'endpoint_service_id': '1', + 'vpc_id': '2', + 'subnet_id': '3', + 'port_ip': '4', + 'routetables': ['abc', '123'], + 'whitelist': ['xyz', '456'], + 'specification_name': '5', + 'description': '6', + 'tags': [{'key': 'tag-key', 'value': 'tag-value'}], + 'enable_dns': True, + 'enable_whitelist': True, + } + + self.client.create_endpoint.assert_called_with(**attrs) + self.assertEqual(self.columns, columns) + + +class TestShowEndpoint(fakes.TestVpcep): + + _data = fakes.FakeEndpoint.create_one() + + columns = ( + 'created_at', + 'endpoint_service_id', + 'endpoint_service_name', + 'id', + 'is_dns_enabled', + 'is_enabled', + 'is_whitelist_enabled', + 'marker_id', + 'project_id', + 'router_id', + 'service_type', + 'status', + 'tags', + 'updated_at', + 'whitelist', + ) + + data = fakes.gen_data(_data, columns, formatters=endpoint._formatters) + + def setUp(self): + super(TestShowEndpoint, self).setUp() + + self.cmd = endpoint.ShowEndpoint(self.app, None) + + self.client.get_endpoint = mock.Mock(return_value=self._data) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Testing that a call without the required argument will fail and + # throw a "ParserExecption" + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_show(self): + arglist = [ + self._data.id, + ] + + verifylist = [ + ('endpoint', self._data.id), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + self.client.get_endpoint.assert_called_with(self._data.id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_show_non_existent(self): + arglist = [ + 'non-existing-endpoint-id', + ] + + verifylist = [ + ('endpoint', 'non-existing-endpoint-id'), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = exceptions.CommandError('Resource Not Found') + self.client.find_service = mock.Mock(side_effect=find_mock_result) + + # Trigger the action + try: + self.cmd.take_action(parsed_args) + except Exception as e: + self.assertEqual('Resource Not Found', str(e)) + self.client.get_endpoint.assert_called_with('non-existing-endpoint-id') + + +class TestDeleteEndpoint(fakes.TestVpcep): + + _data = fakes.FakeEndpoint.create_multiple(2) + + def setUp(self): + super(TestDeleteEndpoint, self).setUp() + + self.client.delete_endpoint = mock.Mock(return_value=None) + + # Get the command object to test + self.cmd = endpoint.DeleteEndpoint(self.app, None) + + def test_delete(self): + arglist = [ + self._data[0].id, + ] + + verifylist = [ + ('endpoint', [self._data[0].id]), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Trigger the action + result = self.cmd.take_action(parsed_args) + self.client.delete_endpoint.assert_called_with(self._data[0].id) + self.assertIsNone(result) + + def test_multiple_delete(self): + arglist = [] + + for data in self._data: + arglist.append(data.id) + + verifylist = [ + ('endpoint', arglist), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Trigger the action + result = self.cmd.take_action(parsed_args) + + calls = [] + for data in self._data: + calls.append(call(data.id)) + self.client.delete_endpoint.assert_has_calls(calls) + self.assertIsNone(result) + + def test_multiple_delete_with_exception(self): + arglist = [ + self._data[0].id, + 'non-existing-endpoint-id', + ] + verifylist = [ + ('endpoint', arglist), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + delete_mock_result = [None, exceptions.CommandError] + self.client.delete_endpoint = mock.Mock(side_effect=delete_mock_result) + + # Trigger the action + try: + self.cmd.take_action(parsed_args) + except Exception as e: + self.assertEqual( + '1 of 2 VPC endpoint(s) failed to delete.', str(e) + ) + + self.client.delete_endpoint.assert_any_call(self._data[0].id) + self.client.delete_endpoint.assert_any_call('non-existing-endpoint-id') diff --git a/otcextensions/tests/unit/osclient/vpcep/v1/test_quota.py b/otcextensions/tests/unit/osclient/vpcep/v1/test_quota.py new file mode 100644 index 000000000..239ed1d98 --- /dev/null +++ b/otcextensions/tests/unit/osclient/vpcep/v1/test_quota.py @@ -0,0 +1,72 @@ +# 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.vpcep.v1 import quota +from otcextensions.tests.unit.osclient.vpcep.v1 import fakes + + +class TestListQuota(fakes.TestVpcep): + + objects = fakes.FakeQuota.create_multiple(2) + column_list_headers = ('Type', 'Quota', 'Used') + + data = [] + + for s in objects: + data.append((s.type, s.quota, s.used)) + + def setUp(self): + super(TestListQuota, self).setUp() + + self.cmd = quota.ListQuota(self.app, None) + + self.client.resource_quota = mock.Mock() + self.client.api_mock = self.client.resource_quota + + def test_list(self): + arglist = [] + + verifylist = [] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with(None) + + self.assertEqual(self.column_list_headers, columns) + self.assertEqual(self.data, list(data)) + + def test_list_args(self): + arglist = ['--type', 'endpoint'] + + verifylist = [ + ('type', 'endpoint'), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with('endpoint') diff --git a/otcextensions/tests/unit/osclient/vpcep/v1/test_service.py b/otcextensions/tests/unit/osclient/vpcep/v1/test_service.py new file mode 100644 index 000000000..c5880b4c0 --- /dev/null +++ b/otcextensions/tests/unit/osclient/vpcep/v1/test_service.py @@ -0,0 +1,474 @@ +# 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 unittest.mock import call + +import mock +from openstackclient.tests.unit import utils as tests_utils +from osc_lib import exceptions + +from otcextensions.osclient.vpcep.v1 import service +from otcextensions.tests.unit.osclient.vpcep.v1 import fakes + + +class TestListServices(fakes.TestVpcep): + + objects = fakes.FakeService.create_multiple(3) + + column_list_headers = ( + 'Id', + 'Service Name', + 'Service Type', + 'Server Type', + 'Connection Count', + 'Status', + ) + + columns = ( + 'id', + 'service_name', + 'service_type', + 'server_type', + 'connection_count', + 'status', + ) + + data = [] + + for s in objects: + data.append( + ( + s.id, + s.service_name, + s.service_type, + s.server_type, + s.connection_count, + s.status, + ) + ) + + def setUp(self): + super(TestListServices, self).setUp() + + self.cmd = service.ListServices(self.app, None) + + self.client.services = mock.Mock() + self.client.api_mock = self.client.services + + def test_list(self): + arglist = [] + + verifylist = [] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + 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 = [ + '--id', + '1', + '--name', + '2', + '--status', + '3', + '--sort-key', + 'created_at', + '--sort-dir', + 'desc', + '--limit', + '6', + '--offset', + '7', + ] + + verifylist = [ + ('id', '1'), + ('name', '2'), + ('status', '3'), + ('sort_key', 'created_at'), + ('sort_dir', 'desc'), + ('limit', 6), + ('offset', 7), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with( + id='1', + name='2', + status='3', + sort_key='created_at', + sort_dir='desc', + limit=6, + offset=7, + ) + + +class TestCreateService(fakes.TestVpcep): + + _data = fakes.FakeService.create_one() + + columns = ( + 'created_at', + 'id', + 'is_approval_enabled', + 'pool_id', + 'port_id', + 'ports', + 'project_id', + 'router_id', + 'server_type', + 'service_name', + 'service_type', + 'status', + ) + + data = fakes.gen_data(_data, columns, formatters=service._formatters) + + def setUp(self): + super(TestCreateService, self).setUp() + + self.cmd = service.CreateService(self.app, None) + self.client.create_service = mock.Mock( + return_value=fakes.FakeService.create_one() + ) + + def test_create(self): + arglist = [ + 'test-endpoint-service', + '--port-id', + 'test-port-uuid', + '--pool-id', + 'test-pool-uuid', + '--router-id', + 'test-router-uuid', + '--disable-approval', + '--server-type', + 'VM', + '--service-type', + 'Interface', + '--ports', + 'client_port=80,server_port=80,protocol=TCP', + '--tags', + 'key=tag-key,value=tag-value', + '--tcp-proxy', + 'open', + ] + verifylist = [ + ('name', 'test-endpoint-service'), + ('port_id', 'test-port-uuid'), + ('pool_id', 'test-pool-uuid'), + ('vpc_id', 'test-router-uuid'), + ('disable_approval', True), + ('server_type', 'VM'), + ('service_type', 'interface'), + ( + 'ports', + [ + { + 'client_port': '80', + 'server_port': '80', + 'protocol': 'TCP', + } + ], + ), + ('tags', [{'key': 'tag-key', 'value': 'tag-value'}]), + ('tcp_proxy', 'open'), + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + attrs = { + 'service_name': 'test-endpoint-service', + 'port_id': 'test-port-uuid', + 'pool_id': 'test-pool-uuid', + 'vpc_id': 'test-router-uuid', + 'approval_enabled': False, + 'server_type': 'VM', + 'service_type': 'interface', + 'ports': [ + {'client_port': 80, 'server_port': 80, 'protocol': 'TCP'} + ], + 'tags': [{'key': 'tag-key', 'value': 'tag-value'}], + 'tcp_proxy': 'open', + } + + self.client.create_service.assert_called_with(**attrs) + self.assertEqual(self.columns, columns) + + +class TestShowService(fakes.TestVpcep): + + _data = fakes.FakeService.create_one() + + columns = ( + 'created_at', + 'id', + 'is_approval_enabled', + 'pool_id', + 'port_id', + 'ports', + 'project_id', + 'router_id', + 'server_type', + 'service_name', + 'service_type', + 'status', + ) + + data = fakes.gen_data(_data, columns, formatters=service._formatters) + + def setUp(self): + super(TestShowService, self).setUp() + + self.cmd = service.ShowService(self.app, None) + + self.client.find_service = mock.Mock(return_value=self._data) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Testing that a call without the required argument will fail and + # throw a "ParserExecption" + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_show(self): + arglist = [ + self._data.id, + ] + + verifylist = [ + ('service', self._data.id), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + self.client.find_service.assert_called_with(self._data.id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_show_non_existent(self): + arglist = [ + 'non-existing-service', + ] + + verifylist = [ + ('service', 'non-existing-service'), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = exceptions.CommandError('Resource Not Found') + self.client.find_service = mock.Mock(side_effect=find_mock_result) + + # Trigger the action + try: + self.cmd.take_action(parsed_args) + except Exception as e: + self.assertEqual('Resource Not Found', str(e)) + self.client.find_service.assert_called_with('non-existing-service') + + +class TestUpdateService(fakes.TestVpcep): + + _data = fakes.FakeService.create_one() + + columns = ( + 'created_at', + 'id', + 'is_approval_enabled', + 'pool_id', + 'port_id', + 'ports', + 'project_id', + 'router_id', + 'server_type', + 'service_name', + 'service_type', + 'status', + ) + + data = fakes.gen_data(_data, columns, formatters=service._formatters) + + def setUp(self): + super(TestUpdateService, self).setUp() + + self.cmd = service.UpdateService(self.app, None) + + self.client.find_service = mock.Mock(return_value=self._data) + self.client.update_service = mock.Mock(return_value=self._data) + + def test_update(self): + arglist = [ + self._data.name, + '--name', + 'test-endpoint-service', + '--ports', + 'client_port=80,server_port=80,protocol=TCP', + '--port-id', + 'test-port-uuid', + '--tcp-proxy', + 'open', + '--enable-approval', + ] + verifylist = [ + ('service', self._data.name), + ('service_name', 'test-endpoint-service'), + ( + 'ports', + [ + { + 'client_port': '80', + 'server_port': '80', + 'protocol': 'TCP', + } + ], + ), + ('port_id', 'test-port-uuid'), + ('tcp_proxy', 'open'), + ('enable_approval', True), + ] + # Verify cm is triggereg with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.find_service.assert_called_with(self._data.name) + + attrs = { + 'service_name': 'test-endpoint-service', + 'port_id': 'test-port-uuid', + 'ports': [ + {'client_port': 80, 'server_port': 80, 'protocol': 'TCP'} + ], + 'tcp_proxy': 'open', + 'approval_enabled': True, + } + self.client.update_service.assert_called_with(self._data, **attrs) + self.assertEqual(self.columns, columns) + + +class TestDeleteService(fakes.TestVpcep): + + _data = fakes.FakeService.create_multiple(2) + + def setUp(self): + super(TestDeleteService, self).setUp() + + self.client.delete_service = mock.Mock(return_value=None) + + # Get the command object to test + self.cmd = service.DeleteService(self.app, None) + + def test_delete(self): + arglist = [ + self._data[0].name, + ] + + verifylist = [ + ('service', [self._data[0].name]), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.client.find_service = mock.Mock(return_value=self._data[0]) + + # Trigger the action + result = self.cmd.take_action(parsed_args) + self.client.delete_service.assert_called_with(self._data[0].id) + self.assertIsNone(result) + + def test_multiple_delete(self): + arglist = [] + + for data in self._data: + arglist.append(data.name) + + verifylist = [ + ('service', arglist), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = self._data + self.client.find_service = mock.Mock(side_effect=find_mock_result) + + # Trigger the action + result = self.cmd.take_action(parsed_args) + + calls = [] + for data in self._data: + calls.append(call(data.id)) + self.client.delete_service.assert_has_calls(calls) + self.assertIsNone(result) + + def test_multiple_delete_with_exception(self): + arglist = [ + self._data[0].name, + 'non-existing-service', + ] + verifylist = [ + ('service', arglist), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self._data[0], exceptions.CommandError] + self.client.find_service = mock.Mock(side_effect=find_mock_result) + + # Trigger the action + try: + self.cmd.take_action(parsed_args) + except Exception as e: + self.assertEqual( + '1 of 2 Vpc Endpoint Services(s) failed to delete.', str(e) + ) + + self.client.find_service.assert_any_call(self._data[0].name) + self.client.find_service.assert_any_call('non-existing-service') + self.client.delete_service.assert_called_once_with(self._data[0].id) diff --git a/otcextensions/tests/unit/osclient/vpcep/v1/test_whitelist.py b/otcextensions/tests/unit/osclient/vpcep/v1/test_whitelist.py new file mode 100644 index 000000000..cb0b43d4a --- /dev/null +++ b/otcextensions/tests/unit/osclient/vpcep/v1/test_whitelist.py @@ -0,0 +1,185 @@ +# 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.vpcep.v1 import whitelist +from otcextensions.tests.unit.osclient.vpcep.v1 import fakes + + +class TestListWhitelist(fakes.TestVpcep): + + _service = fakes.FakeService.create_one() + objects = fakes.FakeWhitelist.create_multiple(3) + column_list_headers = ('Id', 'Permission', 'Created At') + + columns = ( + 'id', + 'persmission', + 'created_at', + ) + + data = [] + + for s in objects: + data.append( + ( + s.id, + s.permission, + s.created_at, + ) + ) + + def setUp(self): + super(TestListWhitelist, self).setUp() + + self.cmd = whitelist.ListWhitelist(self.app, None) + + self.client.find_service = mock.Mock(return_value=self._service) + self.client.service_whitelist = mock.Mock() + self.client.api_mock = self.client.service_whitelist + + def test_list(self): + arglist = [self._service.name] + + verifylist = [ + ('service', self._service.name), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with(self._service) + + self.assertEqual(self.column_list_headers, columns) + self.assertEqual(self.data, list(data)) + + def test_list_args(self): + arglist = [ + self._service.name, + '--sort-key', + 'created_at', + '--sort-dir', + 'asc', + '--limit', + '2', + '--offset', + '3', + ] + + verifylist = [ + ('service', self._service.name), + ('sort_key', 'created_at'), + ('sort_dir', 'asc'), + ('limit', 2), + ('offset', 3), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with( + self._service, + sort_key='created_at', + sort_dir='asc', + limit=2, + offset=3, + ) + + +class TestManageWhitelist(fakes.TestVpcep): + + _service = fakes.FakeService.create_one() + objects = fakes.FakeWhitelist.create_multiple(3) + column_list_headers = ('Permission',) + + columns = ('persmission',) + + data = [] + + for s in objects: + data.append((s.permission,)) + + def setUp(self): + super(TestManageWhitelist, self).setUp() + + self.cmd = whitelist.ManageWhitelist(self.app, None) + + self.client.find_service = mock.Mock(return_value=self._service) + self.client.manage_service_whitelist = mock.Mock() + self.client.api_mock = self.client.manage_service_whitelist + + def test_add_whitelist(self): + arglist = [self._service.name, '123', 'xyz', '--add'] + + verifylist = [ + ('service', self._service.name), + ('domain', ['123', 'xyz']), + ('add', True), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with( + self._service, + domains=['123', 'xyz'], + action='add', + ) + + self.assertEqual(self.column_list_headers, columns) + self.assertEqual(self.data, list(data)) + + def test_remove_whitelist(self): + arglist = [self._service.name, '123', 'xyz', '--remove'] + + verifylist = [ + ('service', self._service.name), + ('domain', ['123', 'xyz']), + ('remove', True), + ] + + # Verify cm is triggered with default parameters + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Set the response + self.client.api_mock.side_effect = [self.objects] + + # Trigger the action + columns, data = self.cmd.take_action(parsed_args) + + self.client.api_mock.assert_called_with( + self._service, + domains=['123', 'xyz'], + action='remove', + ) + + self.assertEqual(self.column_list_headers, columns) + self.assertEqual(self.data, list(data)) diff --git a/otcextensions/tests/unit/sdk/base.py b/otcextensions/tests/unit/sdk/base.py index b4b6d65f2..f0725bf38 100644 --- a/otcextensions/tests/unit/sdk/base.py +++ b/otcextensions/tests/unit/sdk/base.py @@ -25,16 +25,17 @@ def setUp(self): self.cloud.config.config['ddsv3_api_version'] = '3' def get_keystone_v3_token( - self, - project_name='admin', + self, + project_name='admin', ): ets_rds = self.os_fixture._get_endpoint_templates('rdsv3') svc_rds = self.os_fixture.v3_token.add_service('rdsv3', name='rdsv3') svc_rds.add_standard_endpoints(region='RegionOne', **ets_rds) ets_cce = self.os_fixture._get_endpoint_templates('ccev2.0') - svc_cce = self.os_fixture.v3_token.add_service('ccev2.0', - name='ccev2.0') + svc_cce = self.os_fixture.v3_token.add_service( + 'ccev2.0', name='ccev2.0' + ) svc_cce.add_standard_endpoints(region='RegionOne', **ets_cce) ets_dds = self.os_fixture._get_endpoint_templates('ddsv3') @@ -43,24 +44,34 @@ def get_keystone_v3_token( return super(TestCase, self).get_keystone_v3_token() - def get_rds_url(self, resource=None, - append=None, base_url_append=None, - qs_elements=None): + def get_rds_url( + self, + resource=None, + append=None, + base_url_append=None, + qs_elements=None, + ): url = self.get_mock_url( - 'rdsv3', resource=resource, - append=append, base_url_append=base_url_append, - qs_elements=qs_elements) + 'rdsv3', + resource=resource, + append=append, + base_url_append=base_url_append, + qs_elements=qs_elements, + ) url = url % {'project_id': self.cloud.current_project_id} return url - def get_cce_url(self, resource=None, - append=None, base_url_append=None, - qs_elements=None): + def get_cce_url( + self, + resource=None, + append=None, + base_url_append=None, + qs_elements=None, + ): endpoint_url = ( - 'https://ccev2.0.example.com/' - 'api/v3/projects/%(project_id)s' + 'https://ccev2.0.example.com/' 'api/v3/projects/%(project_id)s' ) % {'project_id': self.cloud.current_project_id} # Strip trailing slashes, so as not to produce double-slashes below if endpoint_url.endswith('/'): @@ -76,13 +87,20 @@ def get_cce_url(self, resource=None, qs = '?%s' % '&'.join(qs_elements) return '%(uri)s%(qs)s' % {'uri': '/'.join(to_join), 'qs': qs} - def get_dds_url(self, resource=None, - append=None, base_url_append=None, - qs_elements=None): + def get_dds_url( + self, + resource=None, + append=None, + base_url_append=None, + qs_elements=None, + ): url = self.get_mock_url( - 'ddsv3', resource=resource, - append=append, base_url_append=base_url_append, - qs_elements=qs_elements) + 'ddsv3', + resource=resource, + append=append, + base_url_append=base_url_append, + qs_elements=qs_elements, + ) url = url % {'project_id': self.cloud.current_project_id} diff --git a/otcextensions/tests/unit/sdk/utils.py b/otcextensions/tests/unit/sdk/utils.py new file mode 100644 index 000000000..4879337f1 --- /dev/null +++ b/otcextensions/tests/unit/sdk/utils.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. +# + + +def assert_attributes_equal(test_case, sot, data): + if isinstance(data, dict): + try: + test_case.assertEqual(sot, data) + except AssertionError: + for k, v in data.items(): + if isinstance(v, list): + assert_attributes_equal(test_case, getattr(sot, k), v) + else: + try: + test_case.assertEqual(getattr(sot, k), v) + except AssertionError: + assert_attributes_equal(test_case, getattr(sot, k), v) + elif isinstance(data, list): + try: + test_case.assertEqual(sot, data) + except AssertionError: + for ix, list_data in enumerate(data): + assert_attributes_equal(test_case, sot[ix], list_data) diff --git a/otcextensions/tests/unit/sdk/vpcep/__init__.py b/otcextensions/tests/unit/sdk/vpcep/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/unit/sdk/vpcep/v1/__init__.py b/otcextensions/tests/unit/sdk/vpcep/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/otcextensions/tests/unit/sdk/vpcep/v1/test_connection.py b/otcextensions/tests/unit/sdk/vpcep/v1/test_connection.py new file mode 100644 index 000000000..61b56aec7 --- /dev/null +++ b/otcextensions/tests/unit/sdk/vpcep/v1/test_connection.py @@ -0,0 +1,112 @@ +# 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 keystoneauth1 import adapter + +from openstack.tests.unit import base +from otcextensions.sdk.vpcep.v1 import connection +from otcextensions.tests.unit.sdk.utils import assert_attributes_equal + +VPCEP_SERVICE_ID = uuid.uuid4().hex + +EXAMPLE = { + 'id': uuid.uuid4().hex, + 'status': 'accepted', + 'marker_id': 16777510, + 'domain_id': '5fc973eea581490997e82ea11a1df31f', + 'created_at': '2018-09-17T11:10:11Z', + 'updated_at': '2018-09-17T11:10:12Z', +} + + +class TestConnection(base.TestCase): + + def setUp(self): + super(TestConnection, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + + def test_basic(self): + sot = connection.Connection() + self.assertEqual('connections', sot.resources_key) + self.assertEqual(None, sot.resource_key) + self.assertEqual( + '/vpc-endpoint-services/%(endpoint_service_id)s/connections', + sot.base_path, + ) + + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + + self.assertDictEqual( + { + 'id': 'id', + 'limit': 'limit', + 'marker': 'marker', + 'marker_id': 'marker_id', + 'offset': 'offset', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = connection.Connection(**EXAMPLE) + assert_attributes_equal(self, sot, EXAMPLE) + + def test_action(self): + sot = connection.Connection.existing( + id=None, endpoint_service_id=VPCEP_SERVICE_ID + ) + action = 'receive' + endpoints = ['123', '456'] + json_body = {'endpoints': endpoints, 'action': action} + response = mock.Mock() + response.status_code = 200 + response.json.return_value = {'connections': [EXAMPLE]} + response.headers = {} + self.sess.post.return_value = response + + rt = list(sot._action(self.sess, action, endpoints)) + self.sess.post.assert_called_with( + 'vpc-endpoint-services/%s/connections/action' % VPCEP_SERVICE_ID, + json=json_body, + ) + self.assertEqual(rt, [connection.Connection.existing(**EXAMPLE)]) + + def test_receive(self): + sot = connection.Connection.existing( + id=None, endpoint_service_id=VPCEP_SERVICE_ID + ) + sot._action = mock.Mock() + + endpoints = ['123', '456'] + rt = sot.accept(self.sess, endpoints) + sot._action.assert_called_with(self.sess, 'receive', endpoints) + self.assertIsNotNone(rt) + + def test_reject(self): + sot = connection.Connection.existing( + id=None, endpoint_service_id=VPCEP_SERVICE_ID + ) + sot._action = mock.Mock() + + endpoints = ['123', '456'] + rt = sot.reject(self.sess, endpoints) + sot._action.assert_called_with(self.sess, 'reject', endpoints) + self.assertIsNotNone(rt) diff --git a/otcextensions/tests/unit/sdk/vpcep/v1/test_endpoint.py b/otcextensions/tests/unit/sdk/vpcep/v1/test_endpoint.py new file mode 100644 index 000000000..af0304feb --- /dev/null +++ b/otcextensions/tests/unit/sdk/vpcep/v1/test_endpoint.py @@ -0,0 +1,97 @@ +# 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 + +from openstack.tests.unit import base +from otcextensions.sdk.vpcep.v1 import endpoint +from otcextensions.tests.unit.sdk.utils import assert_attributes_equal + +EXAMPLE = { + 'active_status': ['active'], + 'created_at': '2024-02-26T14:10:12Z', + 'description': '', + 'enable_dns': False, + 'enable_status': 'enable', + 'enable_whitelist': False, + 'endpoint_pool_id': uuid.uuid4().hex, + 'endpoint_service_id': uuid.uuid4().hex, + 'endpoint_service_name': 'eu-de.css-762-az1.' + uuid.uuid4().hex, + 'id': uuid.uuid4().hex, + 'ip': '192.168.0.232', + 'marker_id': 16952019, + 'project_id': uuid.uuid4().hex, + 'routetables': [], + 'service_type': 'interface', + 'specification_name': 'default', + 'status': 'accepted', + 'subnet_id': uuid.uuid4().hex, + 'tags': [ + { + 'key': 'test1', + 'value': 'test1', + } + ], + 'updated_at': '2024-02-26T14:10:14Z', + 'vpc_id': uuid.uuid4().hex, + 'whitelist': [], +} + + +class TestEndpoint(base.TestCase): + + def test_basic(self): + sot = endpoint.Endpoint() + self.assertEqual('endpoints', sot.resources_key) + self.assertEqual(None, sot.resource_key) + self.assertEqual('/vpc-endpoints', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual( + { + 'id': 'id', + 'limit': 'limit', + 'router_id': 'vpc_id', + 'marker': 'marker', + 'offset': 'offset', + 'endpoint_service_name': 'endpoint_service_name', + 'sort_key': 'sort_key', + 'sort_dir': 'sort_dir', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + updated_sot_attrs = { + 'enable_dns': 'is_dns_enabled', + 'enable_whitelist': 'is_whitelist_enabled', + 'routetables': 'route_tables', + 'vpc_id': 'router_id', + 'subnet_id': 'network_id', + 'enable_status': 'is_enabled', + } + if 'enable_status' in EXAMPLE.keys(): + enable_status = EXAMPLE['enable_status'] + EXAMPLE['enable_status'] = ( + True if enable_status == 'enable' else False + ) + + sot = endpoint.Endpoint(**EXAMPLE) + for key, value in EXAMPLE.items(): + if key in updated_sot_attrs.keys(): + self.assertEqual(getattr(sot, updated_sot_attrs[key]), value) + else: + assert_attributes_equal(self, getattr(sot, key), value) diff --git a/otcextensions/tests/unit/sdk/vpcep/v1/test_proxy.py b/otcextensions/tests/unit/sdk/vpcep/v1/test_proxy.py new file mode 100644 index 000000000..3d98b89a2 --- /dev/null +++ b/otcextensions/tests/unit/sdk/vpcep/v1/test_proxy.py @@ -0,0 +1,158 @@ +# 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 test_proxy_base +from otcextensions.sdk.vpcep.v1 import _proxy +from otcextensions.sdk.vpcep.v1 import connection +from otcextensions.sdk.vpcep.v1 import endpoint +from otcextensions.sdk.vpcep.v1 import quota +from otcextensions.sdk.vpcep.v1 import service +from otcextensions.sdk.vpcep.v1 import whitelist + + +class TestVpcepProxy(test_proxy_base.TestProxyBase): + def setUp(self): + super(TestVpcepProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + +class TestEndpointService(TestVpcepProxy): + def test_list_services(self): + self.verify_list( + self.proxy.services, + service.Service, + ) + + def test_create_service(self): + self.verify_create( + self.proxy.create_service, + service.Service, + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id'}, + ) + + def test_get_service(self): + self.verify_get( + self.proxy.get_service, + service.Service, + ) + + def test_find_service(self): + self.verify_find( + self.proxy.find_service, + service.Service, + ) + + def test_update_service(self): + self.verify_update( + self.proxy.update_service, + service.Service, + ) + + def test_delete_service(self): + self.verify_delete( + self.proxy.delete_service, + service.Service, + True, + ) + + +class TestEndpoint(TestVpcepProxy): + def test_endpoints(self): + self.verify_list(self.proxy.endpoints, endpoint.Endpoint) + + def test_endpoint_create(self): + self.verify_create( + self.proxy.create_endpoint, + endpoint.Endpoint, + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id'}, + ) + + def test_endpoint_get(self): + self.verify_get(self.proxy.get_endpoint, endpoint.Endpoint) + + def test_endpoint_delete(self): + self.verify_delete(self.proxy.delete_endpoint, endpoint.Endpoint, True) + + +class TestWhitelist(TestVpcepProxy): + def test_service_whitelist(self): + self.verify_list( + self.proxy.service_whitelist, + whitelist.Whitelist, + method_kwargs={'service': 'endpoint-service-id'}, + expected_kwargs={'endpoint_service_id': 'endpoint-service-id'}, + ) + + def test_manage_service_whitelist_add(self): + self._verify( + 'otcextensions.sdk.vpcep.v1.whitelist.Whitelist.add', + self.proxy.manage_service_whitelist, + method_kwargs={ + 'service': 'endpoint-service-id', + 'action': 'add', + 'domains': ['abc', 'xyz'], + }, + expected_args=[self.proxy, ['abc', 'xyz']], + ) + + def test_manage_service_whitelist_remove(self): + self._verify( + 'otcextensions.sdk.vpcep.v1.whitelist.Whitelist.remove', + self.proxy.manage_service_whitelist, + method_kwargs={ + 'service': 'endpoint-service-id', + 'action': 'remove', + 'domains': ['abc', 'xyz'], + }, + expected_args=[self.proxy, ['abc', 'xyz']], + ) + + +class TestConnection(TestVpcepProxy): + def test_service_connections(self): + self.verify_list( + self.proxy.service_connections, + connection.Connection, + method_kwargs={'service': 'endpoint-service-id'}, + expected_kwargs={'endpoint_service_id': 'endpoint-service-id'}, + ) + + def test_manage_service_connections_accept(self): + self._verify( + 'otcextensions.sdk.vpcep.v1.connection.Connection.accept', + self.proxy.manage_service_connections, + method_kwargs={ + 'service': 'endpoint-service-id', + 'action': 'accept', + 'endpoints': ['abc', 'xyz'], + }, + expected_args=[self.proxy, ['abc', 'xyz']], + ) + + def test_manage_service_connections_reject(self): + self._verify( + 'otcextensions.sdk.vpcep.v1.connection.Connection.reject', + self.proxy.manage_service_connections, + method_kwargs={ + 'service': 'endpoint-service-id', + 'action': 'reject', + 'endpoints': ['abc', 'xyz'], + }, + expected_args=[self.proxy, ['abc', 'xyz']], + ) + + +class TestQuota(TestVpcepProxy): + def test_resource_quota(self): + self.verify_list(self.proxy.resource_quota, quota.Quota) diff --git a/otcextensions/tests/unit/sdk/vpcep/v1/test_quota.py b/otcextensions/tests/unit/sdk/vpcep/v1/test_quota.py new file mode 100644 index 000000000..14b054919 --- /dev/null +++ b/otcextensions/tests/unit/sdk/vpcep/v1/test_quota.py @@ -0,0 +1,50 @@ +# 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.vpcep.v1 import quota +from otcextensions.tests.unit.sdk.utils import assert_attributes_equal + +EXAMPLE = { + 'type': 'endpoint', + 'quota': 150, + 'used': 14, +} + + +class TestQuota(base.TestCase): + def setUp(self): + super(TestQuota, self).setUp() + + def test_basic(self): + sot = quota.Quota() + self.assertEqual(None, sot.resources_key) + self.assertEqual(None, sot.resource_key) + self.assertEqual('/quotas', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + + self.assertDictEqual( + { + 'limit': 'limit', + 'marker': 'marker', + 'type': 'type', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = quota.Quota(**EXAMPLE) + assert_attributes_equal(self, sot, EXAMPLE) diff --git a/otcextensions/tests/unit/sdk/vpcep/v1/test_service.py b/otcextensions/tests/unit/sdk/vpcep/v1/test_service.py new file mode 100644 index 000000000..aa9c53572 --- /dev/null +++ b/otcextensions/tests/unit/sdk/vpcep/v1/test_service.py @@ -0,0 +1,80 @@ +# 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 + +from openstack.tests.unit import base +from otcextensions.sdk.vpcep.v1 import service +from otcextensions.tests.unit.sdk.utils import assert_attributes_equal + +EXAMPLE = { + 'id': uuid.uuid4().hex, + 'port_id': uuid.uuid4().hex, + 'vpc_id': uuid.uuid4().hex, + 'pool_id': uuid.uuid4().hex, + 'status': 'available', + 'approval_enabled': False, + 'service_name': 'test123', + 'service_type': 'interface', + 'server_type': 'VM', + 'project_id': uuid.uuid4().hex, + 'created_at': '2018-01-30T07:42:01.174', + 'ports': [ + {'client_port': 8080, 'server_port': 90, 'protocol': 'TCP'}, + {'client_port': 8081, 'server_port': 80, 'protocol': 'TCP'}, + ], +} + + +class TestEndpointService(base.TestCase): + + def setUp(self): + super(TestEndpointService, self).setUp() + + def test_basic(self): + sot = service.Service() + self.assertEqual('endpoint_services', sot.resources_key) + self.assertEqual(None, sot.resource_key) + self.assertEqual('/vpc-endpoint-services', sot.base_path) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + self.assertDictEqual( + { + 'id': 'id', + 'limit': 'limit', + 'marker': 'marker', + 'name': 'endpoint_service_name', + 'offset': 'offset', + 'sort_dir': 'sort_dir', + 'sort_key': 'sort_key', + 'status': 'status', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = service.Service(**EXAMPLE) + updated_sot_attrs = { + 'approval_enabled': 'is_approval_enabled', + 'vpc_id': 'router_id', + } + for key, value in EXAMPLE.items(): + if key in updated_sot_attrs.keys(): + self.assertEqual( + getattr(sot, updated_sot_attrs[key]), EXAMPLE[key] + ) + else: + assert_attributes_equal(self, getattr(sot, key), value) diff --git a/otcextensions/tests/unit/sdk/vpcep/v1/test_whitelist.py b/otcextensions/tests/unit/sdk/vpcep/v1/test_whitelist.py new file mode 100644 index 000000000..7cf6e6544 --- /dev/null +++ b/otcextensions/tests/unit/sdk/vpcep/v1/test_whitelist.py @@ -0,0 +1,100 @@ +# 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 keystoneauth1 import adapter + +from openstack.tests.unit import base +from otcextensions.sdk.vpcep.v1 import whitelist +from otcextensions.tests.unit.sdk.utils import assert_attributes_equal + +VPCEP_SERVICE_ID = uuid.uuid4().hex + +EXAMPLE = { + 'id': uuid.uuid4().hex, + 'permission': '*', + 'created_at': '2018-10-18T13:26:40Z', +} + + +class TestWhitelist(base.TestCase): + def setUp(self): + super(TestWhitelist, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + + def test_basic(self): + sot = whitelist.Whitelist() + self.assertEqual('permissions', sot.resources_key) + self.assertEqual(None, sot.resource_key) + self.assertEqual( + '/vpc-endpoint-services/%(endpoint_service_id)s/permissions', + sot.base_path, + ) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_fetch) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + + def test_make_it(self): + sot = whitelist.Whitelist(**EXAMPLE) + assert_attributes_equal(self, sot, EXAMPLE) + + def test_action(self): + sot = whitelist.Whitelist.existing( + id=None, endpoint_service_id=VPCEP_SERVICE_ID + ) + action = 'add' + domains = ['123', '456'] + formatted_domains = [] + for domain in domains: + formatted_domains.append('iam:domain::' + domain) + json_body = {'permissions': formatted_domains, 'action': action} + response = mock.Mock() + response.status_code = 200 + response.json.return_value = {'permissions': formatted_domains} + response.headers = {} + self.sess.post.return_value = response + + rt = list(sot._action(self.sess, action, domains)) + self.sess.post.assert_called_with( + 'vpc-endpoint-services/%s/permissions/action' % VPCEP_SERVICE_ID, + json=json_body, + ) + self.assertEqual( + rt[0], + whitelist.Whitelist.existing(permission=formatted_domains[0]), + ) + + def test_add(self): + sot = whitelist.Whitelist.existing( + id=None, endpoint_service_id=VPCEP_SERVICE_ID + ) + sot._action = mock.Mock() + + domains = ['123', '456'] + rt = sot.add(self.sess, domains) + sot._action.assert_called_with(self.sess, 'add', domains) + self.assertIsNotNone(rt) + + def test_remove(self): + sot = whitelist.Whitelist.existing( + id=None, endpoint_service_id=VPCEP_SERVICE_ID + ) + sot._action = mock.Mock() + + domains = ['123', '456'] + rt = sot.remove(self.sess, domains) + sot._action.assert_called_with(self.sess, 'remove', domains) + self.assertIsNotNone(rt) diff --git a/releasenotes/notes/vpcep-module-d76cfde52e8e1711.yaml b/releasenotes/notes/vpcep-module-d76cfde52e8e1711.yaml new file mode 100644 index 000000000..bad44d8f8 --- /dev/null +++ b/releasenotes/notes/vpcep-module-d76cfde52e8e1711.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The update contains all SDK and OpenStack Client parts for the + 'VPC Endpoint' module and the unit tests. diff --git a/setup.cfg b/setup.cfg index 44c87c57c..c13c77c21 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,7 @@ openstack.cli.extension = mrs = otcextensions.osclient.mrs.client nat = otcextensions.osclient.nat.client vpc = otcextensions.osclient.vpc.client + vpcep = otcextensions.osclient.vpcep.client dlb = otcextensions.osclient.vlb.client iam = otcextensions.osclient.identity.client smn = otcextensions.osclient.smn.client @@ -447,6 +448,22 @@ openstack.vpc.v2 = vpc_route_add = otcextensions.osclient.vpc.v2.route:AddVpcRoute vpc_route_delete = otcextensions.osclient.vpc.v2.route:DeleteVpcRoute +openstack.vpcep.v1 = + vpcep_quota_list = otcextensions.osclient.vpcep.v1.quota:ListQuota + vpcep_endpoint_list = otcextensions.osclient.vpcep.v1.endpoint:ListEndpoints + vpcep_endpoint_show = otcextensions.osclient.vpcep.v1.endpoint:ShowEndpoint + vpcep_endpoint_create = otcextensions.osclient.vpcep.v1.endpoint:CreateEndpoint + vpcep_endpoint_delete = otcextensions.osclient.vpcep.v1.endpoint:DeleteEndpoint + vpcep_service_list = otcextensions.osclient.vpcep.v1.service:ListServices + vpcep_service_show = otcextensions.osclient.vpcep.v1.service:ShowService + vpcep_service_create = otcextensions.osclient.vpcep.v1.service:CreateService + vpcep_service_update = otcextensions.osclient.vpcep.v1.service:UpdateService + vpcep_service_delete = otcextensions.osclient.vpcep.v1.service:DeleteService + vpcep_service_whitelist_list = otcextensions.osclient.vpcep.v1.whitelist:ListWhitelist + vpcep_service_whitelist_set = otcextensions.osclient.vpcep.v1.whitelist:ManageWhitelist + vpcep_service_connection_list = otcextensions.osclient.vpcep.v1.connection:ListConnections + vpcep_service_connection_set = otcextensions.osclient.vpcep.v1.connection:ManageConnections + openstack.vlb.v3 = dlb_loadbalancer_list = otcextensions.osclient.vlb.v3.load_balancer:ListLoadBalancers dlb_loadbalancer_show = otcextensions.osclient.vlb.v3.load_balancer:ShowLoadBalancer