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
This commit is contained in:
@@ -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
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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):
|
||||
|
@@ -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)
|
||||
|
@@ -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.
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
30
openstack/network/v2/tag.py
Normal file
30
openstack/network/v2/tag.py
Normal file
@@ -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
|
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
|
44
openstack/tests/unit/network/v2/test_tag.py
Normal file
44
openstack/tests/unit/network/v2/test_tag.py
Normal file
@@ -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']})
|
Reference in New Issue
Block a user