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:
Akihiro Motoki
2017-04-28 23:07:17 +00:00
parent bc4a0c4066
commit a87fde8ea7
11 changed files with 158 additions and 6 deletions

View File

@@ -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
^^^^^^^^^^^^^^

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)

View 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

View File

@@ -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)

View File

@@ -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)

View 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']})