From c2f2ffdd9f91773838060280747614b1691e7e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dulko?= Date: Wed, 26 Feb 2020 14:07:19 +0100 Subject: [PATCH] Implement If-Match support for Neutron resources Neutron API supports using If-Match HTTP header to do compare-and-swap updates and deletes of several resources [1]. This feature is based on revision_number property. This commit implements that by adding the if_revision argument to supported update_* and delete_* resources. [1] https://docs.openstack.org/api-ref/network/v2/?expanded=list-routers-detail#revisions Change-Id: I5c4ff42796cc860c0a99a431cac84bb75a2d9236 --- openstack/compute/v2/server.py | 2 +- openstack/database/v1/user.py | 2 +- openstack/exceptions.py | 7 + openstack/image/v2/image.py | 2 +- openstack/load_balancer/v2/quota.py | 2 +- openstack/network/v2/_base.py | 27 +++ openstack/network/v2/_proxy.py | 109 +++++++++--- openstack/network/v2/floating_ip.py | 5 +- openstack/network/v2/network.py | 5 +- openstack/network/v2/port.py | 5 +- openstack/network/v2/quota.py | 2 +- openstack/network/v2/router.py | 3 +- openstack/network/v2/security_group.py | 5 +- openstack/network/v2/security_group_rule.py | 5 +- openstack/network/v2/subnet.py | 5 +- openstack/resource.py | 19 +- openstack/tests/unit/network/v2/test_proxy.py | 166 +++++++++++++++--- openstack/tests/unit/test_resource.py | 2 + 18 files changed, 290 insertions(+), 83 deletions(-) create mode 100644 openstack/network/v2/_base.py diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index c74f57e76..7064d8be8 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -196,7 +196,7 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): vm_state = resource.Body('OS-EXT-STS:vm_state') def _prepare_request(self, requires_id=True, prepend_key=True, - base_path=None): + base_path=None, **kwargs): request = super(Server, self)._prepare_request(requires_id=requires_id, prepend_key=prepend_key, base_path=base_path) diff --git a/openstack/database/v1/user.py b/openstack/database/v1/user.py index b9bd4fe14..07e6fc72a 100644 --- a/openstack/database/v1/user.py +++ b/openstack/database/v1/user.py @@ -35,7 +35,7 @@ class User(resource.Resource): password = resource.Body('password') def _prepare_request(self, requires_id=True, prepend_key=True, - base_path=None): + base_path=None, **kwargs): """Prepare a request for the database service's create call User.create calls require the resources_key. diff --git a/openstack/exceptions.py b/openstack/exceptions.py index 56fba3b5e..a69d97729 100644 --- a/openstack/exceptions.py +++ b/openstack/exceptions.py @@ -121,6 +121,11 @@ class ConflictException(HttpException): pass +class PreconditionFailedException(HttpException): + """HTTP 412 Precondition Failed.""" + pass + + class MethodNotSupported(SDKException): """The resource does not support this operation type.""" def __init__(self, resource, method): @@ -192,6 +197,8 @@ def raise_from_response(response, error_message=None): cls = NotFoundException elif response.status_code == 400: cls = BadRequestException + elif response.status_code == 412: + cls = PreconditionFailedException else: cls = HttpException diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index dd789d5ba..3e0f223d2 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -285,7 +285,7 @@ class Image(resource.Resource, resource.TagMixin, _download.DownloadMixin): session.post(url, json=json, headers=headers) def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False, base_path=None): + patch=False, base_path=None, **kwargs): request = super(Image, self)._prepare_request(requires_id=requires_id, prepend_key=prepend_key, patch=patch, diff --git a/openstack/load_balancer/v2/quota.py b/openstack/load_balancer/v2/quota.py index a14f62812..8a0106970 100644 --- a/openstack/load_balancer/v2/quota.py +++ b/openstack/load_balancer/v2/quota.py @@ -42,7 +42,7 @@ class Quota(resource.Resource): project_id = resource.Body('project_id', alternate_id=True) def _prepare_request(self, requires_id=True, - base_path=None, prepend_key=False): + base_path=None, prepend_key=False, **kwargs): _request = super(Quota, self)._prepare_request(requires_id, prepend_key, base_path=base_path) diff --git a/openstack/network/v2/_base.py b/openstack/network/v2/_base.py new file mode 100644 index 000000000..147563d26 --- /dev/null +++ b/openstack/network/v2/_base.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. +from openstack import resource + + +class NetworkResource(resource.Resource): + #: Revision number of the resource. *Type: int* + revision_number = resource.Body('revision_number', type=int) + + def _prepare_request(self, requires_id=None, prepend_key=False, + patch=False, base_path=None, params=None, + if_revision=None, **kwargs): + req = super(NetworkResource, self)._prepare_request( + requires_id=requires_id, prepend_key=prepend_key, patch=patch, + base_path=base_path, params=params) + if if_revision is not None: + req.headers['If-Match'] = "revision_number=%d" % if_revision + return req diff --git a/openstack/network/v2/_proxy.py b/openstack/network/v2/_proxy.py index 835945cf1..f6f69580c 100644 --- a/openstack/network/v2/_proxy.py +++ b/openstack/network/v2/_proxy.py @@ -60,6 +60,26 @@ from openstack import proxy class Proxy(proxy.Proxy): + @proxy._check_resource(strict=False) + def _update(self, resource_type, value, base_path=None, + if_revision=None, **attrs): + res = self._get_resource(resource_type, value, **attrs) + return res.commit(self, base_path=base_path, if_revision=if_revision) + + @proxy._check_resource(strict=False) + def _delete(self, resource_type, value, ignore_missing=True, + if_revision=None, **attrs): + res = self._get_resource(resource_type, value, **attrs) + + try: + rv = res.delete(self, if_revision=if_revision) + except exceptions.ResourceNotFound: + if ignore_missing: + return None + raise + + return rv + def create_address_scope(self, **attrs): """Create a new address scope from attributes @@ -497,7 +517,7 @@ class Proxy(proxy.Proxy): """ return self._create(_floating_ip.FloatingIP, **attrs) - def delete_ip(self, floating_ip, ignore_missing=True): + def delete_ip(self, floating_ip, ignore_missing=True, if_revision=None): """Delete a floating ip :param floating_ip: The value can be either the ID of a floating ip @@ -508,11 +528,13 @@ class Proxy(proxy.Proxy): raised when the floating ip does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent ip. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ self._delete(_floating_ip.FloatingIP, floating_ip, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, if_revision=if_revision) def find_available_ip(self): """Find an available IP @@ -577,19 +599,22 @@ class Proxy(proxy.Proxy): """ return self._list(_floating_ip.FloatingIP, **query) - def update_ip(self, floating_ip, **attrs): + def update_ip(self, floating_ip, if_revision=None, **attrs): """Update a ip :param floating_ip: Either the id of a ip or a :class:`~openstack.network.v2.floating_ip.FloatingIP` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the ip represented by ``value``. :returns: The updated ip :rtype: :class:`~openstack.network.v2.floating_ip.FloatingIP` """ - return self._update(_floating_ip.FloatingIP, floating_ip, **attrs) + return self._update(_floating_ip.FloatingIP, floating_ip, + if_revision=if_revision, **attrs) def create_port_forwarding(self, **attrs): """Create a new floating ip port forwarding from attributes @@ -1203,7 +1228,7 @@ class Proxy(proxy.Proxy): """ return self._create(_network.Network, **attrs) - def delete_network(self, network, ignore_missing=True): + def delete_network(self, network, ignore_missing=True, if_revision=None): """Delete a network :param network: @@ -1214,10 +1239,13 @@ class Proxy(proxy.Proxy): raised when the network does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent network. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ - self._delete(_network.Network, network, ignore_missing=ignore_missing) + self._delete(_network.Network, network, ignore_missing=ignore_missing, + if_revision=if_revision) def find_network(self, name_or_id, ignore_missing=True, **args): """Find a single network @@ -1276,18 +1304,21 @@ class Proxy(proxy.Proxy): """ return self._list(_network.Network, **query) - def update_network(self, network, **attrs): + def update_network(self, network, if_revision=None, **attrs): """Update a network :param network: Either the id of a network or an instance of type :class:`~openstack.network.v2.network.Network`. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the network represented by ``network``. :returns: The updated network :rtype: :class:`~openstack.network.v2.network.Network` """ - return self._update(_network.Network, network, **attrs) + return self._update(_network.Network, network, if_revision=if_revision, + **attrs) def find_network_ip_availability(self, name_or_id, ignore_missing=True, **args): @@ -1699,7 +1730,7 @@ class Proxy(proxy.Proxy): """ return self._bulk_create(_port.Port, data) - def delete_port(self, port, ignore_missing=True): + def delete_port(self, port, ignore_missing=True, if_revision=None): """Delete a port :param port: The value can be either the ID of a port or a @@ -1709,10 +1740,13 @@ class Proxy(proxy.Proxy): raised when the port does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent port. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ - self._delete(_port.Port, port, ignore_missing=ignore_missing) + self._delete(_port.Port, port, ignore_missing=ignore_missing, + if_revision=if_revision) def find_port(self, name_or_id, ignore_missing=True, **args): """Find a single port @@ -1766,18 +1800,21 @@ class Proxy(proxy.Proxy): """ return self._list(_port.Port, **query) - def update_port(self, port, **attrs): + def update_port(self, port, if_revision=None, **attrs): """Update a port :param port: Either the id of a port or a :class:`~openstack.network.v2.port.Port` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the port represented by ``port``. :returns: The updated port :rtype: :class:`~openstack.network.v2.port.Port` """ - return self._update(_port.Port, port, **attrs) + return self._update(_port.Port, port, if_revision=if_revision, + **attrs) def add_ip_to_port(self, port, ip): ip.port_id = port.id @@ -2482,7 +2519,7 @@ class Proxy(proxy.Proxy): """ return self._create(_router.Router, **attrs) - def delete_router(self, router, ignore_missing=True): + def delete_router(self, router, ignore_missing=True, if_revision=None): """Delete a router :param router: The value can be either the ID of a router or a @@ -2492,10 +2529,13 @@ class Proxy(proxy.Proxy): raised when the router does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent router. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ - self._delete(_router.Router, router, ignore_missing=ignore_missing) + self._delete(_router.Router, router, ignore_missing=ignore_missing, + if_revision=if_revision) def find_router(self, name_or_id, ignore_missing=True, **args): """Find a single router @@ -2546,18 +2586,21 @@ class Proxy(proxy.Proxy): """ return self._list(_router.Router, **query) - def update_router(self, router, **attrs): + def update_router(self, router, if_revision=None, **attrs): """Update a router :param router: Either the id of a router or a :class:`~openstack.network.v2.router.Router` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the router represented by ``router``. :returns: The updated router :rtype: :class:`~openstack.network.v2.router.Router` """ - return self._update(_router.Router, router, **attrs) + return self._update(_router.Router, router, if_revision=if_revision, + **attrs) def add_interface_to_router(self, router, subnet_id=None, port_id=None): """Add Interface to a router @@ -3048,7 +3091,8 @@ class Proxy(proxy.Proxy): """ return self._create(_security_group.SecurityGroup, **attrs) - def delete_security_group(self, security_group, ignore_missing=True): + def delete_security_group(self, security_group, ignore_missing=True, + if_revision=None): """Delete a security group :param security_group: @@ -3060,11 +3104,13 @@ class Proxy(proxy.Proxy): raised when the security group does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent security group. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ self._delete(_security_group.SecurityGroup, security_group, - ignore_missing=ignore_missing) + ignore_missing=ignore_missing, if_revision=if_revision) def find_security_group(self, name_or_id, ignore_missing=True, **args): """Find a single security group @@ -3113,12 +3159,14 @@ class Proxy(proxy.Proxy): """ return self._list(_security_group.SecurityGroup, **query) - def update_security_group(self, security_group, **attrs): + def update_security_group(self, security_group, if_revision=None, **attrs): """Update a security group :param security_group: Either the id of a security group or a :class:`~openstack.network.v2.security_group.SecurityGroup` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the security group represented by ``security_group``. @@ -3126,7 +3174,7 @@ class Proxy(proxy.Proxy): :rtype: :class:`~openstack.network.v2.security_group.SecurityGroup` """ return self._update(_security_group.SecurityGroup, security_group, - **attrs) + if_revision=if_revision, **attrs) def create_security_group_rule(self, **attrs): """Create a new security group rule from attributes @@ -3143,7 +3191,7 @@ class Proxy(proxy.Proxy): return self._create(_security_group_rule.SecurityGroupRule, **attrs) def delete_security_group_rule(self, security_group_rule, - ignore_missing=True): + ignore_missing=True, if_revision=None): """Delete a security group rule :param security_group_rule: @@ -3155,11 +3203,14 @@ class Proxy(proxy.Proxy): raised when the security group rule does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent security group rule. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ self._delete(_security_group_rule.SecurityGroupRule, - security_group_rule, ignore_missing=ignore_missing) + security_group_rule, ignore_missing=ignore_missing, + if_revision=if_revision) def find_security_group_rule(self, name_or_id, ignore_missing=True, **args): @@ -3424,7 +3475,7 @@ class Proxy(proxy.Proxy): """ return self._create(_subnet.Subnet, **attrs) - def delete_subnet(self, subnet, ignore_missing=True): + def delete_subnet(self, subnet, ignore_missing=True, if_revision=None): """Delete a subnet :param subnet: The value can be either the ID of a subnet or a @@ -3434,10 +3485,13 @@ class Proxy(proxy.Proxy): raised when the subnet does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent subnet. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :returns: ``None`` """ - self._delete(_subnet.Subnet, subnet, ignore_missing=ignore_missing) + self._delete(_subnet.Subnet, subnet, ignore_missing=ignore_missing, + if_revision=if_revision) def find_subnet(self, name_or_id, ignore_missing=True, **args): """Find a single subnet @@ -3491,18 +3545,21 @@ class Proxy(proxy.Proxy): """ return self._list(_subnet.Subnet, **query) - def update_subnet(self, subnet, **attrs): + def update_subnet(self, subnet, if_revision=None, **attrs): """Update a subnet :param subnet: Either the id of a subnet or a :class:`~openstack.network.v2.subnet.Subnet` instance. + :param int if_revision: Revision to put in If-Match header of update + request to perform compare-and-swap update. :param dict attrs: The attributes to update on the subnet represented by ``subnet``. :returns: The updated subnet :rtype: :class:`~openstack.network.v2.subnet.Subnet` """ - return self._update(_subnet.Subnet, subnet, **attrs) + return self._update(_subnet.Subnet, subnet, if_revision=if_revision, + **attrs) def create_subnet_pool(self, **attrs): """Create a new subnet pool from attributes diff --git a/openstack/network/v2/floating_ip.py b/openstack/network/v2/floating_ip.py index 68a563fed..2f94226f1 100644 --- a/openstack/network/v2/floating_ip.py +++ b/openstack/network/v2/floating_ip.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class FloatingIP(resource.Resource, resource.TagMixin): +class FloatingIP(_base.NetworkResource, resource.TagMixin): name_attribute = "floating_ip_address" resource_name = "floating ip" resource_key = 'floatingip' @@ -68,8 +69,6 @@ class FloatingIP(resource.Resource, resource.TagMixin): qos_policy_id = resource.Body('qos_policy_id') #: The ID of the project this floating IP is associated with. project_id = resource.Body('tenant_id') - #: Revision number of the floating IP. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: The ID of an associated router. router_id = resource.Body('router_id') #: The floating IP status. Value is ``ACTIVE`` or ``DOWN``. diff --git a/openstack/network/v2/network.py b/openstack/network/v2/network.py index 3339c1f9e..884c52a86 100644 --- a/openstack/network/v2/network.py +++ b/openstack/network/v2/network.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class Network(resource.Resource, resource.TagMixin): +class Network(_base.NetworkResource, resource.TagMixin): resource_key = 'network' resources_key = 'networks' base_path = '/networks' @@ -97,8 +98,6 @@ class Network(resource.Resource, resource.TagMixin): provider_segmentation_id = resource.Body('provider:segmentation_id') #: The ID of the QoS policy attached to the port. qos_policy_id = resource.Body('qos_policy_id') - #: Revision number of the network. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: A list of provider segment objects. #: Available for multiple provider extensions. segments = resource.Body('segments') diff --git a/openstack/network/v2/port.py b/openstack/network/v2/port.py index 1f96f4329..800b91fe5 100644 --- a/openstack/network/v2/port.py +++ b/openstack/network/v2/port.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class Port(resource.Resource, resource.TagMixin): +class Port(_base.NetworkResource, resource.TagMixin): resource_key = 'port' resources_key = 'ports' base_path = '/ports' @@ -114,8 +115,6 @@ class Port(resource.Resource, resource.TagMixin): # (i.e.: minimum-bandwidth) and traits (i.e.: vnic-type, physnet) # requested by a port to Nova and Placement. resource_request = resource.Body('resource_request', type=dict) - #: Revision number of the port. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: The IDs of any attached security groups. #: *Type: list of strs of the security group IDs* security_group_ids = resource.Body('security_groups', type=list) diff --git a/openstack/network/v2/quota.py b/openstack/network/v2/quota.py index 87432718c..fbda19bc0 100644 --- a/openstack/network/v2/quota.py +++ b/openstack/network/v2/quota.py @@ -57,7 +57,7 @@ class Quota(resource.Resource): security_groups = resource.Body('security_group', type=int) def _prepare_request(self, requires_id=True, prepend_key=False, - base_path=None): + base_path=None, **kwargs): _request = super(Quota, self)._prepare_request(requires_id, prepend_key) if self.resource_key in _request.body: diff --git a/openstack/network/v2/router.py b/openstack/network/v2/router.py index a7de75f8b..b714b6e48 100644 --- a/openstack/network/v2/router.py +++ b/openstack/network/v2/router.py @@ -11,11 +11,12 @@ # under the License. from openstack import exceptions +from openstack.network.v2 import _base from openstack import resource from openstack import utils -class Router(resource.Resource, resource.TagMixin): +class Router(_base.NetworkResource, resource.TagMixin): resource_key = 'router' resources_key = 'routers' base_path = '/routers' diff --git a/openstack/network/v2/security_group.py b/openstack/network/v2/security_group.py index bb456e7f4..f4c60dc7a 100644 --- a/openstack/network/v2/security_group.py +++ b/openstack/network/v2/security_group.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class SecurityGroup(resource.Resource, resource.TagMixin): +class SecurityGroup(_base.NetworkResource, resource.TagMixin): resource_key = 'security_group' resources_key = 'security_groups' base_path = '/security-groups' @@ -40,8 +41,6 @@ class SecurityGroup(resource.Resource, resource.TagMixin): name = resource.Body('name') #: The ID of the project this security group is associated with. project_id = resource.Body('project_id') - #: Revision number of the security group. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: A list of #: :class:`~openstack.network.v2.security_group_rule.SecurityGroupRule` #: objects. *Type: list* diff --git a/openstack/network/v2/security_group_rule.py b/openstack/network/v2/security_group_rule.py index 391b97eb1..398366e5c 100644 --- a/openstack/network/v2/security_group_rule.py +++ b/openstack/network/v2/security_group_rule.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class SecurityGroupRule(resource.Resource, resource.TagMixin): +class SecurityGroupRule(_base.NetworkResource, resource.TagMixin): resource_key = 'security_group_rule' resources_key = 'security_group_rules' base_path = '/security-group-rules' @@ -74,8 +75,6 @@ class SecurityGroupRule(resource.Resource, resource.TagMixin): #: in the request body. This attribute matches the specified IP prefix #: as the source IP address of the IP packet. remote_ip_prefix = resource.Body('remote_ip_prefix') - #: Revision number of the security group rule. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: The security group ID to associate with this security group rule. security_group_id = resource.Body('security_group_id') #: The ID of the project this security group rule is associated with. diff --git a/openstack/network/v2/subnet.py b/openstack/network/v2/subnet.py index 2b1d3ad18..8ddbdbe4d 100644 --- a/openstack/network/v2/subnet.py +++ b/openstack/network/v2/subnet.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.network.v2 import _base from openstack import resource -class Subnet(resource.Resource, resource.TagMixin): +class Subnet(_base.NetworkResource, resource.TagMixin): resource_key = 'subnet' resources_key = 'subnets' base_path = '/subnets' @@ -75,8 +76,6 @@ class Subnet(resource.Resource, resource.TagMixin): prefix_length = resource.Body('prefixlen') #: The ID of the project this subnet is associated with. project_id = resource.Body('tenant_id') - #: Revision number of the subnet. *Type: int* - revision_number = resource.Body('revision_number', type=int) #: The ID of the segment this subnet is associated with. segment_id = resource.Body('segment_id') #: Service types for this subnet diff --git a/openstack/resource.py b/openstack/resource.py index 9260e9577..6cefb5bd8 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1053,7 +1053,7 @@ class Resource(dict): return body def _prepare_request(self, requires_id=None, prepend_key=False, - patch=False, base_path=None, params=None): + patch=False, base_path=None, params=None, **kwargs): """Prepare a request to be sent to the server Create operations don't require an ID, but all others do, @@ -1436,7 +1436,7 @@ class Resource(dict): or self.allow_empty_commit) def commit(self, session, prepend_key=True, has_body=True, - retry_on_conflict=None, base_path=None): + retry_on_conflict=None, base_path=None, **kwargs): """Commit the state of the instance to the remote resource. :param session: The session to use for making this request. @@ -1450,6 +1450,8 @@ class Resource(dict): :param str base_path: Base part of the URI for modifying resources, if different from :data:`~openstack.resource.Resource.base_path`. + :param dict kwargs: Parameters that will be passed to + _prepare_request() :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1467,7 +1469,6 @@ class Resource(dict): # Avoid providing patch unconditionally to avoid breaking subclasses # without it. - kwargs = {} if self.commit_jsonpatch: kwargs['patch'] = True @@ -1582,11 +1583,13 @@ class Resource(dict): has_body=has_body, retry_on_conflict=retry_on_conflict) - def delete(self, session, error_message=None): + def delete(self, session, error_message=None, **kwargs): """Delete the remote resource based on this instance. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` + :param dict kwargs: Parameters that will be passed to + _prepare_request() :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -1595,7 +1598,7 @@ class Resource(dict): the resource was not found. """ - response = self._raw_delete(session) + response = self._raw_delete(session, **kwargs) kwargs = {} if error_message: kwargs['error_message'] = error_message @@ -1603,16 +1606,16 @@ class Resource(dict): self._translate_response(response, has_body=False, **kwargs) return self - def _raw_delete(self, session): + def _raw_delete(self, session, **kwargs): if not self.allow_delete: raise exceptions.MethodNotSupported(self, "delete") - request = self._prepare_request() + request = self._prepare_request(**kwargs) session = self._get_session(session) microversion = self._get_microversion_for(session, 'delete') return session.delete( - request.url, + request.url, headers=request.headers, microversion=microversion) @classmethod diff --git a/openstack/tests/unit/network/v2/test_proxy.py b/openstack/tests/unit/network/v2/test_proxy.py index f1037bf5e..b5a5bc102 100644 --- a/openstack/tests/unit/network/v2/test_proxy.py +++ b/openstack/tests/unit/network/v2/test_proxy.py @@ -70,6 +70,25 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): super(TestNetworkProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + def verify_update(self, test_method, resource_type, value=None, + mock_method="openstack.network.v2._proxy.Proxy._update", + expected_result="result", path_args=None, **kwargs): + super(TestNetworkProxy, self).verify_update( + test_method, resource_type, value=value, mock_method=mock_method, + expected_result=expected_result, path_args=path_args, **kwargs) + + def verify_delete(self, test_method, resource_type, ignore, + input_path_args=None, expected_path_args=None, + method_kwargs=None, expected_args=None, + expected_kwargs=None, + mock_method="openstack.network.v2._proxy.Proxy._delete"): + super(TestNetworkProxy, self).verify_delete( + test_method, resource_type, ignore, + input_path_args=input_path_args, + expected_path_args=expected_path_args, method_kwargs=method_kwargs, + expected_args=expected_args, expected_kwargs=expected_kwargs, + mock_method=mock_method) + def test_address_scope_create_attrs(self): self.verify_create(self.proxy.create_address_scope, address_scope.AddressScope) @@ -143,11 +162,16 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): def test_floating_ip_delete(self): self.verify_delete(self.proxy.delete_ip, floating_ip.FloatingIP, - False) + False, expected_kwargs={'if_revision': None}) def test_floating_ip_delete_ignore(self): self.verify_delete(self.proxy.delete_ip, floating_ip.FloatingIP, - True) + True, expected_kwargs={'if_revision': None}) + + def test_floating_ip_delete_if_revision(self): + self.verify_delete(self.proxy.delete_ip, floating_ip.FloatingIP, + True, method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_floating_ip_find(self): self.verify_find(self.proxy.find_ip, floating_ip.FloatingIP) @@ -159,7 +183,16 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): self.verify_list(self.proxy.ips, floating_ip.FloatingIP) def test_floating_ip_update(self): - self.verify_update(self.proxy.update_ip, floating_ip.FloatingIP) + self.verify_update(self.proxy.update_ip, floating_ip.FloatingIP, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_floating_ip_update_if_revision(self): + self.verify_update(self.proxy.update_ip, floating_ip.FloatingIP, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) def test_health_monitor_create_attrs(self): self.verify_create(self.proxy.create_health_monitor, @@ -300,10 +333,17 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): self.verify_create(self.proxy.create_network, network.Network) def test_network_delete(self): - self.verify_delete(self.proxy.delete_network, network.Network, False) + self.verify_delete(self.proxy.delete_network, network.Network, False, + expected_kwargs={'if_revision': None}) def test_network_delete_ignore(self): - self.verify_delete(self.proxy.delete_network, network.Network, True) + self.verify_delete(self.proxy.delete_network, network.Network, True, + expected_kwargs={'if_revision': None}) + + def test_network_delete_if_revision(self): + self.verify_delete(self.proxy.delete_network, network.Network, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_network_find(self): self.verify_find(self.proxy.find_network, network.Network) @@ -324,7 +364,16 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): self.verify_list(self.proxy.networks, network.Network) def test_network_update(self): - self.verify_update(self.proxy.update_network, network.Network) + self.verify_update(self.proxy.update_network, network.Network, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_network_update_if_revision(self): + self.verify_update(self.proxy.update_network, network.Network, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) def test_flavor_create_attrs(self): self.verify_create(self.proxy.create_flavor, flavor.Flavor) @@ -417,7 +466,7 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): expected_kwargs={"pool_id": "test_id"}) def test_pool_member_update(self): - self._verify2("openstack.proxy.Proxy._update", + self._verify2("openstack.network.v2._proxy.Proxy._update", self.proxy.update_pool_member, method_args=["MEMBER", "POOL"], expected_args=[pool_member.PoolMember, "MEMBER"], @@ -448,10 +497,17 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): self.verify_create(self.proxy.create_port, port.Port) def test_port_delete(self): - self.verify_delete(self.proxy.delete_port, port.Port, False) + self.verify_delete(self.proxy.delete_port, port.Port, False, + expected_kwargs={'if_revision': None}) def test_port_delete_ignore(self): - self.verify_delete(self.proxy.delete_port, port.Port, True) + self.verify_delete(self.proxy.delete_port, port.Port, True, + expected_kwargs={'if_revision': None}) + + def test_port_delete_if_revision(self): + self.verify_delete(self.proxy.delete_port, port.Port, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_port_find(self): self.verify_find(self.proxy.find_port, port.Port) @@ -463,7 +519,16 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): self.verify_list(self.proxy.ports, port.Port) def test_port_update(self): - self.verify_update(self.proxy.update_port, port.Port) + self.verify_update(self.proxy.update_port, port.Port, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_port_update_if_revision(self): + self.verify_update(self.proxy.update_port, port.Port, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) @mock.patch('openstack.network.v2._proxy.Proxy._bulk_create') def test_ports_create(self, bc): @@ -521,7 +586,7 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): def test_qos_bandwidth_limit_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.Proxy._update', + self._verify2('openstack.network.v2._proxy.Proxy._update', self.proxy.update_qos_bandwidth_limit_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -578,7 +643,7 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): def test_qos_dscp_marking_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.Proxy._update', + self._verify2('openstack.network.v2._proxy.Proxy._update', self.proxy.update_qos_dscp_marking_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -636,7 +701,7 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): def test_qos_minimum_bandwidth_rule_update(self): policy = qos_policy.QoSPolicy.new(id=QOS_POLICY_ID) - self._verify2('openstack.proxy.Proxy._update', + self._verify2('openstack.network.v2._proxy.Proxy._update', self.proxy.update_qos_minimum_bandwidth_rule, method_args=['rule_id', policy], method_kwargs={'foo': 'bar'}, @@ -749,10 +814,17 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): self.verify_create(self.proxy.create_router, router.Router) def test_router_delete(self): - self.verify_delete(self.proxy.delete_router, router.Router, False) + self.verify_delete(self.proxy.delete_router, router.Router, False, + expected_kwargs={'if_revision': None}) def test_router_delete_ignore(self): - self.verify_delete(self.proxy.delete_router, router.Router, True) + self.verify_delete(self.proxy.delete_router, router.Router, True, + expected_kwargs={'if_revision': None}) + + def test_router_delete_if_revision(self): + self.verify_delete(self.proxy.delete_router, router.Router, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_router_find(self): self.verify_find(self.proxy.find_router, router.Router) @@ -764,7 +836,16 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): self.verify_list(self.proxy.routers, router.Router) def test_router_update(self): - self.verify_update(self.proxy.update_router, router.Router) + self.verify_update(self.proxy.update_router, router.Router, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_router_update_if_revision(self): + self.verify_update(self.proxy.update_router, router.Router, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) @mock.patch.object(proxy_base.Proxy, '_get_resource') @mock.patch.object(router.Router, 'add_interface') @@ -1010,11 +1091,19 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): def test_security_group_delete(self): self.verify_delete(self.proxy.delete_security_group, - security_group.SecurityGroup, False) + security_group.SecurityGroup, False, + expected_kwargs={'if_revision': None}) def test_security_group_delete_ignore(self): self.verify_delete(self.proxy.delete_security_group, - security_group.SecurityGroup, True) + security_group.SecurityGroup, True, + expected_kwargs={'if_revision': None}) + + def test_security_group_delete_if_revision(self): + self.verify_delete(self.proxy.delete_security_group, + security_group.SecurityGroup, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_security_group_find(self): self.verify_find(self.proxy.find_security_group, @@ -1030,7 +1119,17 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): def test_security_group_update(self): self.verify_update(self.proxy.update_security_group, - security_group.SecurityGroup) + security_group.SecurityGroup, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) + + def test_security_group_update_if_revision(self): + self.verify_update(self.proxy.update_security_group, + security_group.SecurityGroup, + method_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': 42}) def test_security_group_rule_create_attrs(self): self.verify_create(self.proxy.create_security_group_rule, @@ -1038,11 +1137,19 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): def test_security_group_rule_delete(self): self.verify_delete(self.proxy.delete_security_group_rule, - security_group_rule.SecurityGroupRule, False) + security_group_rule.SecurityGroupRule, False, + expected_kwargs={'if_revision': None}) def test_security_group_rule_delete_ignore(self): self.verify_delete(self.proxy.delete_security_group_rule, - security_group_rule.SecurityGroupRule, True) + security_group_rule.SecurityGroupRule, True, + expected_kwargs={'if_revision': None}) + + def test_security_group_rule_delete_if_revision(self): + self.verify_delete(self.proxy.delete_security_group_rule, + security_group_rule.SecurityGroupRule, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_security_group_rule_find(self): self.verify_find(self.proxy.find_security_group_rule, @@ -1081,10 +1188,17 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): self.verify_create(self.proxy.create_subnet, subnet.Subnet) def test_subnet_delete(self): - self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, False) + self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, False, + expected_kwargs={'if_revision': None}) def test_subnet_delete_ignore(self): - self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, True) + self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, True, + expected_kwargs={'if_revision': None}) + + def test_subnet_delete_if_revision(self): + self.verify_delete(self.proxy.delete_subnet, subnet.Subnet, True, + method_kwargs={'if_revision': 42}, + expected_kwargs={'if_revision': 42}) def test_subnet_find(self): self.verify_find(self.proxy.find_subnet, subnet.Subnet) @@ -1096,7 +1210,9 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): self.verify_list(self.proxy.subnets, subnet.Subnet) def test_subnet_update(self): - self.verify_update(self.proxy.update_subnet, subnet.Subnet) + self.verify_update(self.proxy.update_subnet, subnet.Subnet, + expected_kwargs={'x': 1, 'y': 2, 'z': 3, + 'if_revision': None}) def test_subnet_pool_create_attrs(self): self.verify_create(self.proxy.create_subnet_pool, @@ -1244,7 +1360,7 @@ class TestNetworkProxy(test_proxy_base.TestProxyBase): def test_update_floating_ip_port_forwarding(self): fip = floating_ip.FloatingIP.new(id=FIP_ID) - self._verify2('openstack.proxy.Proxy._update', + self._verify2('openstack.network.v2._proxy.Proxy._update', self.proxy.update_floating_ip_port_forwarding, method_args=[fip, 'port_forwarding_id'], method_kwargs={'foo': 'bar'}, diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 1e47d89af..989ab9984 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1750,6 +1750,7 @@ class TestResourceActions(base.TestCase): self.sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( self.request.url, + headers='headers', microversion=None) self.sot._translate_response.assert_called_once_with( @@ -1772,6 +1773,7 @@ class TestResourceActions(base.TestCase): sot._prepare_request.assert_called_once_with() self.session.delete.assert_called_once_with( self.request.url, + headers='headers', microversion='1.42') sot._translate_response.assert_called_once_with(