summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Herve <therve@redhat.com>2015-12-09 16:45:30 +0100
committerThomas Herve <therve@redhat.com>2016-03-03 09:11:30 +0100
commitcb98419d6218f4ce80bfef47f4abe4baf49b2303 (patch)
treedba6e60bd862b5731784cf5f188ac0b7883f054b
parent7609d19468db1cc03bfa1a9950f0d7b369a919a1 (diff)
downloadheat-cb98419d6218f4ce80bfef47f4abe4baf49b2303.tar.gz
Fix database purge query
Fix raw_template purge query on MySQL, and handle stack tags before removing stacks. This also removes a bunch of race conditions where we deleted incorrect data. Change-Id: I7b7a1d94acefbaeeed86f1833c979819361c8988 Closes-Bug: #1524387 (cherry picked from commit 6cece5bb7e9e10dfb33596c32319e7ad9b81fbff)
-rw-r--r--heat/db/sqlalchemy/api.py99
-rwxr-xr-xheat/objects/stack.py5
-rw-r--r--heat/tests/db/test_sqlalchemy_api.py2
-rw-r--r--heat_integrationtests/common/test.py7
-rw-r--r--heat_integrationtests/functional/test_purge.py50
5 files changed, 124 insertions, 39 deletions
diff --git a/heat/db/sqlalchemy/api.py b/heat/db/sqlalchemy/api.py
index fce12a487..1f8801b47 100644
--- a/heat/db/sqlalchemy/api.py
+++ b/heat/db/sqlalchemy/api.py
@@ -946,13 +946,14 @@ def purge_deleted(age, granularity='days'):
elif granularity == 'minutes':
age = age * 60
- time_line = datetime.datetime.now() - datetime.timedelta(seconds=age)
+ time_line = datetime.datetime.utcnow() - datetime.timedelta(seconds=age)
engine = get_engine()
meta = sqlalchemy.MetaData()
meta.bind = engine
stack = sqlalchemy.Table('stack', meta, autoload=True)
stack_lock = sqlalchemy.Table('stack_lock', meta, autoload=True)
+ stack_tag = sqlalchemy.Table('stack_tag', meta, autoload=True)
resource = sqlalchemy.Table('resource', meta, autoload=True)
resource_data = sqlalchemy.Table('resource_data', meta, autoload=True)
event = sqlalchemy.Table('event', meta, autoload=True)
@@ -962,40 +963,68 @@ def purge_deleted(age, granularity='days'):
syncpoint = sqlalchemy.Table('sync_point', meta, autoload=True)
# find the soft-deleted stacks that are past their expiry
- stack_where = sqlalchemy.select([stack.c.id]).where(
- stack.c.deleted_at < time_line)
- # delete stack locks (just in case some got stuck)
- stack_lock_del = stack_lock.delete().where(
- stack_lock.c.stack_id.in_(stack_where))
- engine.execute(stack_lock_del)
- # delete resource_data
- res_where = sqlalchemy.select([resource.c.id]).where(
- resource.c.stack_id.in_(stack_where))
- res_data_del = resource_data.delete().where(
- resource_data.c.resource_id.in_(res_where))
- engine.execute(res_data_del)
- # delete resources
- res_del = resource.delete().where(resource.c.stack_id.in_(stack_where))
- engine.execute(res_del)
- # delete events
- event_del = event.delete().where(event.c.stack_id.in_(stack_where))
- engine.execute(event_del)
- # clean up any sync_points that may have lingered
- sync_del = syncpoint.delete().where(syncpoint.c.stack_id.in_(stack_where))
- engine.execute(sync_del)
- # delete the stacks
- stack_del = stack.delete().where(stack.c.deleted_at < time_line)
- engine.execute(stack_del)
- # delete orphaned raw templates
- stack_templ_sel = sqlalchemy.select([stack.c.raw_template_id])
- raw_templ_sel = sqlalchemy.not_(raw_template.c.id.in_(stack_templ_sel))
- raw_templ_del = raw_template.delete().where(raw_templ_sel)
- engine.execute(raw_templ_del)
- # purge any user creds that are no longer referenced
- stack_creds_sel = sqlalchemy.select([stack.c.user_creds_id])
- user_creds_sel = sqlalchemy.not_(user_creds.c.id.in_(stack_creds_sel))
- usr_creds_del = user_creds.delete().where(user_creds_sel)
- engine.execute(usr_creds_del)
+ stack_where = sqlalchemy.select([stack.c.id, stack.c.raw_template_id,
+ stack.c.prev_raw_template_id,
+ stack.c.user_creds_id]).where(
+ stack.c.deleted_at < time_line)
+ stacks = list(engine.execute(stack_where))
+ if stacks:
+ stack_ids = [i[0] for i in stacks]
+ # delete stack locks (just in case some got stuck)
+ stack_lock_del = stack_lock.delete().where(
+ stack_lock.c.stack_id.in_(stack_ids))
+ engine.execute(stack_lock_del)
+ # delete stack tags
+ stack_tag_del = stack_tag.delete().where(
+ stack_tag.c.stack_id.in_(stack_ids))
+ engine.execute(stack_tag_del)
+ # delete resource_data
+ res_where = sqlalchemy.select([resource.c.id]).where(
+ resource.c.stack_id.in_(stack_ids))
+ res_data_del = resource_data.delete().where(
+ resource_data.c.resource_id.in_(res_where))
+ engine.execute(res_data_del)
+ # delete resources
+ res_del = resource.delete().where(resource.c.stack_id.in_(stack_ids))
+ engine.execute(res_del)
+ # delete events
+ event_del = event.delete().where(event.c.stack_id.in_(stack_ids))
+ engine.execute(event_del)
+ # clean up any sync_points that may have lingered
+ sync_del = syncpoint.delete().where(
+ syncpoint.c.stack_id.in_(stack_ids))
+ engine.execute(sync_del)
+ # delete the stacks
+ stack_del = stack.delete().where(stack.c.id.in_(stack_ids))
+ engine.execute(stack_del)
+ # delete orphaned raw templates
+ raw_template_ids = [i[1] for i in stacks if i[1] is not None]
+ raw_template_ids.extend(i[2] for i in stacks if i[2] is not None)
+ if raw_template_ids:
+ # keep those still referenced
+ raw_tmpl_sel = sqlalchemy.select([stack.c.raw_template_id]).where(
+ stack.c.raw_template_id.in_(raw_template_ids))
+ raw_tmpl = [i[0] for i in engine.execute(raw_tmpl_sel)]
+ raw_template_ids = set(raw_template_ids) - set(raw_tmpl)
+ raw_tmpl_sel = sqlalchemy.select(
+ [stack.c.prev_raw_template_id]).where(
+ stack.c.prev_raw_template_id.in_(raw_template_ids))
+ raw_tmpl = [i[0] for i in engine.execute(raw_tmpl_sel)]
+ raw_template_ids = raw_template_ids - set(raw_tmpl)
+ raw_templ_del = raw_template.delete().where(
+ raw_template.c.id.in_(raw_template_ids))
+ engine.execute(raw_templ_del)
+ # purge any user creds that are no longer referenced
+ user_creds_ids = [i[3] for i in stacks if i[3] is not None]
+ if user_creds_ids:
+ # keep those still referenced
+ user_sel = sqlalchemy.select([stack.c.user_creds_id]).where(
+ stack.c.user_creds_id.in_(user_creds_ids))
+ users = [i[0] for i in engine.execute(user_sel)]
+ user_creds_ids = set(user_creds_ids) - set(users)
+ usr_creds_del = user_creds.delete().where(
+ user_creds.c.id.in_(user_creds_ids))
+ engine.execute(usr_creds_del)
# Purge deleted services
srvc_del = service.delete().where(service.c.deleted_at < time_line)
engine.execute(srvc_del)
diff --git a/heat/objects/stack.py b/heat/objects/stack.py
index f1bfa9639..fce4d0f89 100755
--- a/heat/objects/stack.py
+++ b/heat/objects/stack.py
@@ -21,6 +21,8 @@ from oslo_versionedobjects import base
from oslo_versionedobjects import fields
+from heat.common import exception
+from heat.common.i18n import _
from heat.db import api as db_api
from heat.objects import fields as heat_fields
from heat.objects import raw_template
@@ -167,6 +169,9 @@ class Stack(
def refresh(self):
db_stack = db_api.stack_get(
self._context, self.id, show_deleted=True)
+ if db_stack is None:
+ message = _('No stack exists with id "%s"') % str(self.id)
+ raise exception.NotFound(message)
db_stack.refresh()
return self.__class__._from_db_object(
self._context,
diff --git a/heat/tests/db/test_sqlalchemy_api.py b/heat/tests/db/test_sqlalchemy_api.py
index f02e54c96..dab356dae 100644
--- a/heat/tests/db/test_sqlalchemy_api.py
+++ b/heat/tests/db/test_sqlalchemy_api.py
@@ -1547,7 +1547,7 @@ class DBAPIStackTest(common.HeatTestCase):
db_api.stack_count_all(self.ctx, tenant_safe=False))
def test_purge_deleted(self):
- now = datetime.datetime.now()
+ now = datetime.datetime.utcnow()
delta = datetime.timedelta(seconds=3600 * 7)
deleted = [now - delta * i for i in range(1, 6)]
templates = [create_raw_template(self.ctx) for i in range(5)]
diff --git a/heat_integrationtests/common/test.py b/heat_integrationtests/common/test.py
index 331c922ce..db9b7d134 100644
--- a/heat_integrationtests/common/test.py
+++ b/heat_integrationtests/common/test.py
@@ -342,7 +342,7 @@ class HeatIntegrationTest(testscenarios.WithScenarios,
return dict((r.resource_name, r.resource_type) for r in resources)
def stack_create(self, stack_name=None, template=None, files=None,
- parameters=None, environment=None,
+ parameters=None, environment=None, tags=None,
expected_status='CREATE_COMPLETE', disable_rollback=True):
name = stack_name or self._stack_rand_name()
templ = template or self.template
@@ -355,10 +355,11 @@ class HeatIntegrationTest(testscenarios.WithScenarios,
files=templ_files,
disable_rollback=disable_rollback,
parameters=params,
- environment=env
+ environment=env,
+ tags=tags
)
if expected_status not in ['ROLLBACK_COMPLETE']:
- self.addCleanup(self.client.stacks.delete, name)
+ self.addCleanup(self._stack_delete, name)
stack = self.client.stacks.get(name)
stack_identifier = '%s/%s' % (name, stack.id)
diff --git a/heat_integrationtests/functional/test_purge.py b/heat_integrationtests/functional/test_purge.py
new file mode 100644
index 000000000..464763a0b
--- /dev/null
+++ b/heat_integrationtests/functional/test_purge.py
@@ -0,0 +1,50 @@
+# 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 subprocess
+
+from heat_integrationtests.common import test
+
+
+class PurgeTest(test.HeatIntegrationTest):
+ template = '''
+heat_template_version: 2014-10-16
+parameters:
+resources:
+ test_resource:
+ type: OS::Heat::TestResource
+'''
+
+ def setUp(self):
+ super(PurgeTest, self).setUp()
+ self.client = self.orchestration_client
+
+ def test_purge(self):
+ stack_identifier = self.stack_create(template=self.template)
+ self._stack_delete(stack_identifier)
+ stacks = dict((stack.id, stack) for stack in
+ self.client.stacks.list(show_deleted=True))
+ self.assertIn(stack_identifier.split('/')[1], stacks)
+ cmd = "heat-manage purge_deleted 0"
+ subprocess.check_call(cmd, shell=True)
+ stacks = dict((stack.id, stack) for stack in
+ self.client.stacks.list(show_deleted=True))
+ self.assertNotIn(stack_identifier.split('/')[1], stacks)
+
+ # Test with tags
+ stack_identifier = self.stack_create(template=self.template,
+ tags="foo,bar")
+ self._stack_delete(stack_identifier)
+ subprocess.check_call(cmd, shell=True)
+ stacks = dict((stack.id, stack) for stack in
+ self.client.stacks.list(show_deleted=True))
+ self.assertNotIn(stack_identifier.split('/')[1], stacks)