Languagepack delete fixup to work with app

1) Modified language_pack_handler's delete method to accommodate
plans and that are created in the background as part
of app deployment invocations.

2) Added a check to ensure that languagepack is deleted only
if it not being used by any apps and any plans

3) Changed a exception wrapper on the language_pack_controller's
methods as it was obscuring the real error message and causing
a spurious exception line to be generated in the solum-api
log (Bug: 1498955)

Change-Id: Iaa71159f9af319e4bab8cdee8d11558a8ab00650
Closes-Bug: #1497414
Closes-Bug: #1498955
This commit is contained in:
Devdatta Kulkarni 2015-09-18 13:52:52 -05:00
parent 090af4633f
commit 4d469df2d9
7 changed files with 116 additions and 14 deletions

View File

View File

@ -0,0 +1,36 @@
# 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 random
import string
def get_sample_data():
data = dict()
s = string.lowercase
data["name"] = "test_app" + ''.join(random.sample(s, 5))
data["description"] = "descp"
data["languagepack"] = "python"
data["trigger_actions"] = ["test", "build", "deploy"]
data["ports"] = [80]
source = {}
source['repository'] = "https://github.com"
source['revision'] = "master"
data["source"] = source
workflow = {}
workflow["test_cmd"] = "./unit_tests.sh"
workflow["run_cmd"] = "python app.py"
data["workflow_config"] = workflow
return data

View File

@ -20,6 +20,7 @@ import time
from tempest_lib import exceptions as tempest_exceptions
from functionaltests.api import base
from functionaltests.api.common import apputils
sample_plan = {"version": "1",
"name": "test_plan",
@ -108,17 +109,35 @@ class TestLanguagePackController(base.TestCase):
self.assertRaises(tempest_exceptions.NotFound,
self.client.delete, 'v1/language_packs/not_found')
def test_language_packs_delete_used_by_app(self):
def test_language_packs_delete_used_by_plan(self):
uuid, sample_lp = self._create_language_pack()
artifacts = sample_plan['artifacts']
artifacts[0]['language_pack'] = sample_lp['name']
sample_plan['artifacts'] = artifacts
print("Sample plan")
print(sample_plan)
resp = self.client.create_plan(data=sample_plan)
self.assertRaises(tempest_exceptions.Conflict,
self.client.delete, 'v1/language_packs/%s' % uuid)
self.client.delete_plan(resp.uuid)
# Sleep for a few seconds to make sure plans are deleted.
time.sleep(5)
self._delete_language_pack(uuid)
def test_language_packs_delete_used_by_app(self):
uuid, sample_lp = self._create_language_pack()
sample_app = apputils.get_sample_data()
sample_app["languagepack"] = sample_lp["name"]
resp = self.client.create_app(data=sample_app)
self.assertRaises(tempest_exceptions.Conflict,
self.client.delete, 'v1/language_packs/%s' % uuid)
bdy = json.loads(resp.body)
self.client.delete_app(bdy["id"])
# Sleep for a few seconds to make sure plans are deleted.
time.sleep(5)
self._delete_language_pack(uuid)

View File

@ -38,7 +38,7 @@ class LanguagePackController(rest.RestController):
logs = userlog_controller.UserlogsController(self._id)
return logs, remainder
@exception.wrap_wsme_pecan_controller_exception
@exception.wrap_pecan_controller_exception
@wsme_pecan.wsexpose(language_pack.LanguagePack)
def get(self):
"""Return a languagepack."""
@ -49,7 +49,7 @@ class LanguagePackController(rest.RestController):
return language_pack.LanguagePack.from_db_model(handler.get(self._id),
host_url)
@exception.wrap_wsme_pecan_controller_exception
@exception.wrap_pecan_controller_exception
@wsme_pecan.wsexpose(status_code=204)
def delete(self):
"""Delete a languagepack."""
@ -67,7 +67,7 @@ class LanguagePacksController(rest.RestController):
remainder = remainder[:-1]
return LanguagePackController(lp_id), remainder
@exception.wrap_wsme_pecan_controller_exception
@exception.wrap_pecan_controller_exception
@wsme_pecan.wsexpose(language_pack.LanguagePack,
body=language_pack.LanguagePack,
status_code=201)
@ -80,7 +80,7 @@ class LanguagePacksController(rest.RestController):
handler.create(data.as_dict(objects.registry.Image),
data.lp_metadata), host_url)
@exception.wrap_wsme_pecan_controller_exception
@exception.wrap_pecan_controller_exception
@wsme_pecan.wsexpose([language_pack.LanguagePack])
def get_all(self):
"""Return all languagepacks, based on the query provided."""

View File

@ -23,9 +23,11 @@ from solum.common import exception as exc
from solum.common import solum_swiftclient
from solum import objects
from solum.objects import image
from solum.objects.sqlalchemy import app
from solum.openstack.common import log as logging
from solum.worker import api
LOG = logging.getLogger(__name__)
# Register options for the service
@ -42,6 +44,30 @@ CONF.register_opts(API_SERVICE_OPTS, group='api')
class LanguagePackHandler(handler.Handler):
"""Fulfills a request on the languagepack resource."""
def _check_lp_referenced_in_any_plan(self, ctxt, db_obj):
plans = objects.registry.PlanList.get_all(ctxt)
for plan in plans:
# (devkulkarni): Adding check for raw_contents because
# there are 'plans' in our db that don't have the
# raw_contents field. These plans correspond to the
# 'dummy' plans that we are creating corresponding
# to 'app deploy' requests.
if hasattr(plan, 'raw_content') and plan.raw_content:
lp = plan.raw_content['artifacts'][0]['language_pack']
if lp == db_obj.name or lp == db_obj.uuid:
return True
def _check_lp_referenced_in_any_app(self, ctxt, db_obj):
apps = app.App.get_all_by_lp(ctxt, db_obj.name)
if len(apps) > 0:
return True
else:
apps = app.App.get_all_by_lp(ctxt, db_obj.uuid)
if len(apps) > 0:
return True
else:
return False
def get(self, id):
"""Return a languagepack."""
return objects.registry.Image.get_lp_by_name_or_uuid(
@ -79,13 +105,10 @@ class LanguagePackHandler(handler.Handler):
"""Delete a languagepack."""
db_obj = objects.registry.Image.get_lp_by_name_or_uuid(self.context,
uuid)
# Check if the languagepack is being used.
plans = objects.registry.PlanList.get_all(self.context)
for plan in plans:
lp = plan.raw_content['artifacts'][0]['language_pack']
if lp == db_obj.name or lp == db_obj.uuid:
raise exc.LPStillReferenced(name=uuid)
if (self._check_lp_referenced_in_any_plan(self.context, db_obj) or
self._check_lp_referenced_in_any_app(self.context, db_obj)):
raise exc.LPStillReferenced(name=uuid)
# Delete image file from swift
if db_obj.docker_image_name:

View File

@ -54,6 +54,23 @@ class App(sql.Base, abstract.App):
app = cls.get_by_id(context, item_uuid)
return app
@classmethod
def get_by_trigger_id(cls, context, trigger_id):
try:
session = sql.Base.get_session()
return session.query(cls).filter_by(trigger_uuid=trigger_id).one()
except sa.orm.exc.NoResultFound:
cls._raise_trigger_not_found(trigger_id)
@classmethod
def get_all_by_lp(cls, context, lp):
session = sql.SolumBase.get_session()
apps = []
with session.begin():
query = session.query(cls).filter_by(languagepack=lp)
apps = sql.filter_by_project(context, query).all()
return apps
@classmethod
@sql.retry
def update_and_save(cls, context, id_or_uuid, data):

View File

@ -60,13 +60,17 @@ class TestLanguagePackHandler(base.BaseTestCase):
@mock.patch('solum.common.solum_swiftclient.SwiftClient.delete_object')
@mock.patch('solum.api.handlers.userlog_handler.UserlogHandler')
@mock.patch('solum.objects.registry.PlanList')
def test_languagepack_delete(self, mock_planlist, mock_log_handler,
@mock.patch('solum.objects.sqlalchemy.app.App')
def test_languagepack_delete(self, mock_app, mock_planlist,
mock_log_handler,
mock_swift_delete, mock_img):
fi = fakes.FakeImage()
mock_img.get_lp_by_name_or_uuid.return_value = fi
mock_img.destroy.return_value = {}
mock_planlist.get_all.return_value = {}
mock_app.get_all_by_lp.return_value = {}
handler = language_pack_handler.LanguagePackHandler(self.ctx)
handler.delete('test_lp')
@ -97,7 +101,9 @@ class TestLanguagePackHandler(base.BaseTestCase):
@mock.patch('solum.common.solum_swiftclient.SwiftClient.delete_object')
@mock.patch('solum.api.handlers.userlog_handler.UserlogHandler')
@mock.patch('solum.objects.registry.PlanList')
@mock.patch('solum.objects.sqlalchemy.app.App')
def test_languagepack_delete_with_plan_not_using_lp(self,
mock_app,
mock_planlist,
mock_log_handler,
mock_swift_delete,
@ -107,6 +113,7 @@ class TestLanguagePackHandler(base.BaseTestCase):
mock_img.destroy.return_value = {}
mock_planlist.get_all.return_value = [fakes.FakePlan()]
mock_app.get_all_by_lp.return_value = {}
handler = language_pack_handler.LanguagePackHandler(self.ctx)
handler.delete('lp_name')