Add cascade delete for APIv2

Change-Id: I96ee5963137cc3b396de538db429bab2963562a6
Closes-Bug: #1663701
This commit is contained in:
Adam Harwell 2017-04-29 09:04:43 +09:00 committed by Michael Johnson
parent 9bfa58af9f
commit b80cc9c923
12 changed files with 125 additions and 35 deletions

View File

@ -1,6 +1,12 @@
###############################################################################
# Path fields
###############################################################################
cascade-delete:
description: |
If ``true`` will delete all child objects of the load balancer.
in: path
required: false
type: boolean
path-listener-id:
description: |
The ID of the listener to query.

View File

@ -203,12 +203,12 @@ Creating a Fully Populated Load Balancer
----------------------------------------
You can configure all documented features of the load balancer at
creation time by specifying the additional elements or attributes in the
request.
creation time by specifying the additional elements or attributes
in the request.
Note: all pools must have names, and must only be fully defined once. To
reference a pool from multiple objects, supply the pool name only for all
subsequent references..
subsequent references.
Request Example
---------------
@ -378,6 +378,9 @@ Remove a load balancer
Removes a load balancer and its associated configuration from the project.
The optional parameter ``cascade`` when defined as ``true`` will delete all
child objects of the load balancer.
The API immediately purges any and all configuration data, depending on the
configuration settings. You cannot recover it.
@ -399,6 +402,7 @@ Request
.. rest_parameters:: ../parameters.yaml
- cascade: cascade-delete
- loadbalancer_id: path-loadbalancer-id
Curl Example

View File

@ -17,6 +17,7 @@ from oslo_config import cfg
from oslo_db import exception as odb_exceptions
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import strutils
import pecan
from wsme import types as wtypes
from wsmeext import pecan as wsme_pecan
@ -323,12 +324,18 @@ class LoadBalancersController(base.BaseController):
result = self._convert_db_to_type(db_lb, lb_types.LoadBalancerResponse)
return lb_types.LoadBalancerRootResponse(loadbalancer=result)
def _delete(self, id, cascade=False):
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204)
def delete(self, id, cascade=False):
"""Deletes a load balancer."""
context = pecan.request.context.get('octavia_context')
cascade = strutils.bool_from_string(cascade)
db_lb = self._get_db_lb(context.session, id)
self._test_lb_status(context.session, id,
lb_status=constants.PENDING_DELETE)
if (db_lb.listeners or db_lb.pools) and not cascade:
msg = _("Cannot delete Load Balancer %s - it has children") % id
LOG.warning(msg)
raise exceptions.ValidationException(detail=msg)
try:
LOG.info("Sending deleted Load Balancer %s to the handler",
@ -341,8 +348,3 @@ class LoadBalancersController(base.BaseController):
provisioning_status=constants.ERROR)
result = self._convert_db_to_type(db_lb, lb_types.LoadBalancerResponse)
return lb_types.LoadBalancersRootResponse(loadbalancer=result)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, id):
"""Deletes a load balancer."""
return self._delete(id)

View File

@ -81,13 +81,16 @@ class PoolFlows(object):
# health monitor should cascade
# members should cascade
delete_pool_flow.add(database_tasks.MarkPoolPendingDeleteInDB(
name='mark_pool_pending_delete_in_db_' + name,
requires=constants.POOL,
rebind={constants.POOL: name}))
delete_pool_flow.add(database_tasks.CountPoolChildrenForQuota(
name='count_pool_children_for_quota_' + name,
requires=constants.POOL,
provides=constants.POOL_CHILD_COUNT,
rebind={constants.POOL: name}))
delete_pool_flow.add(model_tasks.DeleteModelObject(
name='delete_model_object_' + name,
rebind={constants.OBJECT: name}))
delete_pool_flow.add(database_tasks.DeletePoolInDB(
name='delete_pool_in_db_' + name,

View File

@ -110,9 +110,17 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
def _build_body(self, json):
return {self.root_tag: json}
def delete(self, path, headers=None, status=204, expect_errors=False):
def delete(self, path, headers=None, params=None, status=204,
expect_errors=False):
headers = headers or {}
params = params or {}
full_path = self._get_full_path(path)
param_string = ""
for k, v in params.items():
param_string += "{key}={value}&".format(key=k, value=v)
if param_string:
full_path = "{path}?{params}".format(
path=full_path, params=param_string.rstrip("&"))
response = self.app.delete(full_path,
headers=headers,
status=status,

View File

@ -555,7 +555,8 @@ class TestHealthMonitor(base.BaseAPITest):
status=409)
def test_create_when_lb_pending_delete(self):
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
self.create_health_monitor(
self.pool_id,
constants.HEALTH_MONITOR_HTTP, 1, 1, 1, 1, status=409)
@ -565,7 +566,8 @@ class TestHealthMonitor(base.BaseAPITest):
self.pool_id, constants.HEALTH_MONITOR_HTTP,
1, 1, 1, 1).get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
new_hm = {'max_retries': 2}
self.put(self.HM_PATH.format(healthmonitor_id=api_hm.get('id')),
body=self._build_body(new_hm), status=409)
@ -575,6 +577,7 @@ class TestHealthMonitor(base.BaseAPITest):
self.pool_id, constants.HEALTH_MONITOR_HTTP,
1, 1, 1, 1).get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
self.delete(self.HM_PATH.format(healthmonitor_id=api_hm.get('id')),
status=409)

View File

@ -579,7 +579,8 @@ class TestL7Policy(base.BaseAPITest):
status=409)
def test_create_when_lb_pending_delete(self):
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
new_l7policy = {
'listener_id': self.listener_id,
'action': constants.L7POLICY_ACTION_REDIRECT_TO_URL,
@ -592,7 +593,8 @@ class TestL7Policy(base.BaseAPITest):
constants.L7POLICY_ACTION_REJECT,
).get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
new_l7policy = {
'action': constants.L7POLICY_ACTION_REDIRECT_TO_URL,
'redirect_url': 'http://www.example.com'}
@ -605,7 +607,8 @@ class TestL7Policy(base.BaseAPITest):
constants.L7POLICY_ACTION_REJECT,
).get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
self.delete(self.L7POLICY_PATH.format(
l7policy_id=l7policy.get('id')),
status=409)

View File

@ -496,7 +496,8 @@ class TestL7Rule(base.BaseAPITest):
constants.L7RULE_COMPARE_TYPE_STARTS_WITH,
'/api').get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
new_l7rule = {'type': constants.L7RULE_TYPE_HEADER,
'compare_type':
constants.L7RULE_COMPARE_TYPE_STARTS_WITH,
@ -511,7 +512,8 @@ class TestL7Rule(base.BaseAPITest):
constants.L7RULE_COMPARE_TYPE_STARTS_WITH,
'/api').get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
new_l7rule = {'type': constants.L7RULE_TYPE_COOKIE,
'compare_type':
constants.L7RULE_COMPARE_TYPE_ENDS_WITH,
@ -526,6 +528,7 @@ class TestL7Rule(base.BaseAPITest):
constants.L7RULE_COMPARE_TYPE_STARTS_WITH,
'/api').get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
self.delete(self.l7rule_path.format(l7rule_id=l7rule.get('id')),
status=409)

View File

@ -586,17 +586,19 @@ class TestListener(base.BaseAPITest):
lb = self.create_load_balancer(uuidutils.generate_uuid(),
name='lb1', description='desc1',
admin_state_up=False)
self.set_lb_status(lb['loadbalancer']['id'])
lb_id = lb['loadbalancer'].get('id')
self.set_lb_status(lb_id)
lb_listener = {'name': 'listener1', 'description': 'desc1',
'admin_state_up': False,
'protocol': constants.PROTOCOL_HTTP,
'protocol_port': 80, 'connection_limit': 10,
'loadbalancer_id': lb['loadbalancer']['id']}
'loadbalancer_id': lb_id}
body = self._build_body(lb_listener)
api_listener = self.post(
self.LISTENERS_PATH, body).json['listener']
self.set_lb_status(lb['loadbalancer']['id'])
self.delete(self.LB_PATH.format(lb_id=lb['loadbalancer']['id']))
self.LISTENERS_PATH, body).json.get(self.root_tag)
self.set_lb_status(lb_id)
self.delete(self.LB_PATH.format(lb_id=lb_id),
params={'cascade': "true"})
lb_listener_put = {'name': 'listener1_updated'}
body = self._build_body(lb_listener_put)
listener_path = self.LISTENER_PATH.format(
@ -607,17 +609,19 @@ class TestListener(base.BaseAPITest):
lb = self.create_load_balancer(uuidutils.generate_uuid(),
name='lb1', description='desc1',
admin_state_up=False)
self.set_lb_status(lb['loadbalancer']['id'])
lb_id = lb['loadbalancer'].get('id')
self.set_lb_status(lb_id)
lb_listener = {'name': 'listener1', 'description': 'desc1',
'admin_state_up': False,
'protocol': constants.PROTOCOL_HTTP,
'protocol_port': 80, 'connection_limit': 10,
'loadbalancer_id': lb['loadbalancer']['id']}
'loadbalancer_id': lb_id}
body = self._build_body(lb_listener)
api_listener = self.post(
self.LISTENERS_PATH, body).json['listener']
self.set_lb_status(lb['loadbalancer']['id'])
self.delete(self.LB_PATH.format(lb_id=lb['loadbalancer']['id']))
self.LISTENERS_PATH, body).json.get(self.root_tag)
self.set_lb_status(lb_id)
self.delete(self.LB_PATH.format(lb_id=lb_id),
params={'cascade': "true"})
listener_path = self.LISTENER_PATH.format(
listener_id=api_listener['id'])
self.delete(listener_path, status=409)

View File

@ -580,6 +580,54 @@ class TestLoadBalancer(base.BaseAPITest):
api_lb.get('operational_status'))
self.assert_final_lb_statuses(api_lb.get('id'), delete=True)
def test_delete_fails_with_pool(self):
project_id = uuidutils.generate_uuid()
lb = self.create_load_balancer(uuidutils.generate_uuid(),
name='lb1',
project_id=project_id,
description='desc1').get(self.root_tag)
lb_id = lb.get('id')
self.set_lb_status(lb_id)
self.create_pool(
lb_id,
constants.PROTOCOL_HTTP,
constants.LB_ALGORITHM_ROUND_ROBIN)
self.set_lb_status(lb_id)
self.delete(self.LB_PATH.format(lb_id=lb_id), status=400)
def test_delete_fails_with_listener(self):
project_id = uuidutils.generate_uuid()
lb = self.create_load_balancer(uuidutils.generate_uuid(),
name='lb1',
project_id=project_id,
description='desc1').get(self.root_tag)
lb_id = lb.get('id')
self.set_lb_status(lb_id)
self.create_listener(constants.PROTOCOL_HTTP, 80, lb_id)
self.set_lb_status(lb_id)
self.delete(self.LB_PATH.format(lb_id=lb_id), status=400)
def test_cascade_delete(self):
project_id = uuidutils.generate_uuid()
lb = self.create_load_balancer(uuidutils.generate_uuid(),
name='lb1',
project_id=project_id,
description='desc1').get(self.root_tag)
lb_id = lb.get('id')
self.set_lb_status(lb_id)
listener = self.create_listener(
constants.PROTOCOL_HTTP, 80, lb_id).get('listener')
listener_id = listener.get('id')
self.set_lb_status(lb_id)
self.create_pool(
lb_id,
constants.PROTOCOL_HTTP,
constants.LB_ALGORITHM_ROUND_ROBIN,
listener_id=listener_id)
self.set_lb_status(lb_id)
self.delete(self.LB_PATH.format(lb_id=lb_id),
params={'cascade': "true"})
def test_delete_bad_lb_id(self):
path = self.LB_PATH.format(lb_id='bad_uuid')
self.delete(path, status=404)

View File

@ -464,7 +464,8 @@ class TestMember(base.BaseAPITest):
self.create_member(self.pool_id, address="10.0.0.1",
protocol_port=80)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
member = {'address': '10.0.0.2', 'protocol_port': 88,
'project_id': self.project_id}
self.post(self.members_path, body=self._build_body(member),
@ -475,7 +476,8 @@ class TestMember(base.BaseAPITest):
self.pool_id, address="10.0.0.1", protocol_port=80,
name="member1").get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
self.put(self.member_path.format(member_id=member.get('id')),
body=self._build_body({'name': "member2"}), status=409)
@ -484,6 +486,7 @@ class TestMember(base.BaseAPITest):
self.pool_id, address="10.0.0.1",
protocol_port=80).get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
self.delete(self.member_path.format(
member_id=member.get('id')), status=409)

View File

@ -798,7 +798,8 @@ class TestPool(base.BaseAPITest):
status=409)
def test_create_when_lb_pending_delete(self):
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
new_pool = {
'loadbalancer_id': self.lb_id,
'listener_id': self.listener_id,
@ -814,7 +815,8 @@ class TestPool(base.BaseAPITest):
constants.LB_ALGORITHM_ROUND_ROBIN,
listener_id=self.listener_id).get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
new_pool = {'admin_state_up': False}
self.put(self.POOL_PATH.format(pool_id=api_pool.get('id')),
self._build_body(new_pool), status=409)
@ -826,6 +828,7 @@ class TestPool(base.BaseAPITest):
constants.LB_ALGORITHM_ROUND_ROBIN,
listener_id=self.listener_id).get(self.root_tag)
self.set_lb_status(self.lb_id)
self.delete(self.LB_PATH.format(lb_id=self.lb_id))
self.delete(self.LB_PATH.format(lb_id=self.lb_id),
params={'cascade': "true"})
self.delete(self.POOL_PATH.format(pool_id=api_pool.get('id')),
status=409)