From a87fde8ea7da4313a345445ec9c0c5cf9fd67444 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Fri, 28 Apr 2017 23:07:17 +0000 Subject: [PATCH] Network tag support * set_tags() operation for network, subnet, port, subnetpool and router resource. Tag support is implemented as a mixin class as tag support for more resources is being planned. * Tag operation in the network proxy class * Tag related query parameters Tag support in neutron follows API-WG guideline. https://specs.openstack.org/openstack/api-wg/guidelines/tags.html In the API, four operations are defined: replace tags, add a tag, remove a tag, remove all tags, but we can do all operations by using only 'replace tags'. In addition, updating attributes of most network resources is an operation to replace an existing value to a new one, so I believe this applies to 'tags' attribute. Required for blueprint neutron-client-tag Needed-By: Iad59d052f46896d27d73c22d6d4bb3df889f2352 Change-Id: Ibaea97010d152f5491bb9d71b3f9b777ea7019dc --- doc/source/users/proxies/network.rst | 7 +++ openstack/network/v2/_proxy.py | 25 +++++++++++ openstack/network/v2/network.py | 7 ++- openstack/network/v2/port.py | 7 ++- openstack/network/v2/router.py | 7 ++- openstack/network/v2/subnet.py | 7 ++- openstack/network/v2/subnet_pool.py | 7 ++- openstack/network/v2/tag.py | 30 +++++++++++++ .../tests/unit/network/v2/test_network.py | 6 ++- openstack/tests/unit/network/v2/test_proxy.py | 17 +++++++ openstack/tests/unit/network/v2/test_tag.py | 44 +++++++++++++++++++ 11 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 openstack/network/v2/tag.py create mode 100644 openstack/tests/unit/network/v2/test_tag.py diff --git a/doc/source/users/proxies/network.rst b/doc/source/users/proxies/network.rst index 690e8755..290e2010 100644 --- a/doc/source/users/proxies/network.rst +++ b/doc/source/users/proxies/network.rst @@ -332,6 +332,13 @@ Service Profile Operations .. automethod:: openstack.network.v2._proxy.Proxy.associate_flavor_with_service_profile .. automethod:: openstack.network.v2._proxy.Proxy.disassociate_flavor_from_service_profile +Tag Operations +^^^^^^^^^^^^^^ + +.. autoclass:: openstack.network.v2._proxy.Proxy + + .. automethod:: openstack.network.v2._proxy.Proxy.set_tags + VPN Operations ^^^^^^^^^^^^^^ diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index d0d3a936..4ee8559a 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack.network.v2 import address_scope as _address_scope from openstack.network.v2 import agent as _agent from openstack.network.v2 import auto_allocated_topology as \ @@ -2927,6 +2928,30 @@ class Proxy(proxy2.BaseProxy): """ return self._update(_subnet_pool.SubnetPool, subnet_pool, **attrs) + @staticmethod + def _check_tag_support(resource): + try: + # Check 'tags' attribute exists + resource.tags + except AttributeError: + raise exceptions.InvalidRequest( + '%s resource does not support tag' % + resource.__class__.__name__) + + def set_tags(self, resource, tags): + """Replace tags of a specified resource with specified tags + + :param resource: + :class:`~openstack.resource2.Resource` instance. + :param tags: New tags to be set. + :type tags: "list" + + :returns: The updated resource + :rtype: :class:`~openstack.resource2.Resource` + """ + self._check_tag_support(resource) + return resource.set_tags(self._session, tags) + def create_vpn_service(self, **attrs): """Create a new vpn service from attributes diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 33a86e9a..cbdd4518 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource -class Network(resource.Resource): +class Network(resource.Resource, tag.TagMixin): resource_key = 'network' resources_key = 'networks' base_path = '/networks' @@ -39,6 +40,7 @@ class Network(resource.Resource): provider_network_type='provider:network_type', provider_physical_network='provider:physical_network', provider_segmentation_id='provider:segmentation_id', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -111,6 +113,9 @@ class Network(resource.Resource): updated_at = resource.Body('updated_at') #: Indicates the VLAN transparency mode of the network is_vlan_transparent = resource.Body('vlan_transparent', type=bool) + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) class DHCPAgentHostingNetwork(Network): diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index e98de374..b6f94665 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource -class Port(resource.Resource): +class Port(resource.Resource, tag.TagMixin): resource_key = 'port' resources_key = 'ports' base_path = '/ports' @@ -34,6 +35,7 @@ class Port(resource.Resource): is_admin_state_up='admin_state_up', is_port_security_enabled='port_security_enabled', project_id='tenant_id', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -127,3 +129,6 @@ class Port(resource.Resource): trunk_details = resource.Body('trunk_details', type=dict) #: Timestamp when the port was last updated. updated_at = resource.Body('updated_at') + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index 3c21b113..7fa58dba 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -11,11 +11,12 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource from openstack import utils -class Router(resource.Resource): +class Router(resource.Resource, tag.TagMixin): resource_key = 'router' resources_key = 'routers' base_path = '/routers' @@ -35,6 +36,7 @@ class Router(resource.Resource): is_distributed='distributed', is_ha='ha', project_id='tenant_id', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -74,6 +76,9 @@ class Router(resource.Resource): status = resource.Body('status') #: Timestamp when the router was created. updated_at = resource.Body('updated_at') + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) def add_interface(self, session, **body): """Add an internal interface to a logical router. diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 1466017b..a9e95ad5 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource -class Subnet(resource.Resource): +class Subnet(resource.Resource, tag.TagMixin): resource_key = 'subnet' resources_key = 'subnets' base_path = '/subnets' @@ -36,6 +37,7 @@ class Subnet(resource.Resource): project_id='tenant_id', subnet_pool_id='subnetpool_id', use_default_subnet_pool='use_default_subnetpool', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -87,3 +89,6 @@ class Subnet(resource.Resource): 'use_default_subnetpool', type=bool ) + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) diff --git a/openstack/network/v2/subnet_pool.py b/openstack/network/v2/subnet_pool.py index 22b9f60a..86a43590 100644 --- a/openstack/network/v2/subnet_pool.py +++ b/openstack/network/v2/subnet_pool.py @@ -11,10 +11,11 @@ # under the License. from openstack.network import network_service +from openstack.network.v2 import tag from openstack import resource2 as resource -class SubnetPool(resource.Resource): +class SubnetPool(resource.Resource, tag.TagMixin): resource_key = 'subnetpool' resources_key = 'subnetpools' base_path = '/subnetpools' @@ -32,6 +33,7 @@ class SubnetPool(resource.Resource): 'name', is_shared='shared', project_id='tenant_id', + **tag.TagMixin._tag_query_parameters ) # Properties @@ -77,3 +79,6 @@ class SubnetPool(resource.Resource): revision_number = resource.Body('revision_number', type=int) #: Timestamp when the subnet pool was last updated. updated_at = resource.Body('updated_at') + #: A list of assocaited tags + #: *Type: list of tag strings* + tags = resource.Body('tags', type=list) diff --git a/openstack/network/v2/tag.py b/openstack/network/v2/tag.py new file mode 100644 index 00000000..b216e2eb --- /dev/null +++ b/openstack/network/v2/tag.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. + +from openstack import utils + + +class TagMixin(object): + + _tag_query_parameters = { + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', + } + + def set_tags(self, session, tags): + url = utils.urljoin(self.base_path, self.id, 'tags') + session.put(url, endpoint_filter=self.service, + json={'tags': tags}) + self._body.attributes.update({'tags': tags}) + return self diff --git a/openstack/tests/unit/network/v2/test_network.py b/openstack/tests/unit/network/v2/test_network.py index fe90b056..f8fc88b6 100644 --- a/openstack/tests/unit/network/v2/test_network.py +++ b/openstack/tests/unit/network/v2/test_network.py @@ -110,7 +110,11 @@ class TestNetwork(testtools.TestCase): 'is_shared': 'shared', 'provider_network_type': 'provider:network_type', 'provider_physical_network': 'provider:physical_network', - 'provider_segmentation_id': 'provider:segmentation_id' + 'provider_segmentation_id': 'provider:segmentation_id', + 'tags': 'tags', + 'any_tags': 'tags-any', + 'not_tags': 'not-tags', + 'not_any_tags': 'not-tags-any', }, sot._query_mapping._mapping) diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index ff11c8ca..06232cd2 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -14,6 +14,7 @@ import deprecation import mock import uuid +from openstack import exceptions from openstack.network.v2 import _proxy from openstack.network.v2 import address_scope from openstack.network.v2 import agent @@ -1064,3 +1065,19 @@ class TestNetworkProxy(test_proxy_base2.TestProxyBase): auto_allocated_topology.ValidateTopology], expected_kwargs={"project": mock.sentinel.project_id, "requires_id": False}) + + def test_set_tags(self): + x_network = network.Network.new(id='NETWORK_ID') + self._verify('openstack.network.v2.network.Network.set_tags', + self.proxy.set_tags, + method_args=[x_network, ['TAG1', 'TAG2']], + expected_args=[['TAG1', 'TAG2']], + expected_result=mock.sentinel.result_set_tags) + + @mock.patch('openstack.network.v2.network.Network.set_tags') + def test_set_tags_resource_without_tag_suport(self, mock_set_tags): + no_tag_resource = object() + self.assertRaises(exceptions.InvalidRequest, + self.proxy.set_tags, + no_tag_resource, ['TAG1', 'TAG2']) + self.assertEqual(0, mock_set_tags.call_count) diff --git a/openstack/tests/unit/network/v2/test_tag.py b/openstack/tests/unit/network/v2/test_tag.py new file mode 100644 index 00000000..b22ae87d --- /dev/null +++ b/openstack/tests/unit/network/v2/test_tag.py @@ -0,0 +1,44 @@ +# 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 +import testtools + +from openstack.network.v2 import network + + +ID = 'IDENTIFIER' + + +class TestTag(testtools.TestCase): + + @staticmethod + def _create_resource(tags=None): + tags = tags or [] + return network.Network(id=ID, name='test-net', tags=tags) + + def test_tags_attribute(self): + net = self._create_resource() + self.assertTrue(hasattr(net, 'tags')) + self.assertIsInstance(net.tags, list) + + def test_set_tags(self): + net = self._create_resource() + sess = mock.Mock() + result = net.set_tags(sess, ['blue', 'green']) + # Check tags attribute is updated + self.assertEqual(['blue', 'green'], net.tags) + # Check the passed resource is returned + self.assertEqual(net, result) + url = 'networks/' + ID + '/tags' + sess.put.assert_called_once_with(url, endpoint_filter=net.service, + json={'tags': ['blue', 'green']})