Merge "Support CAMP assembly resources"

This commit is contained in:
Jenkins 2015-03-23 18:55:51 +00:00 committed by Gerrit Code Review
commit cae69ca3ec
10 changed files with 469 additions and 27 deletions

View File

@ -14,7 +14,11 @@
import json
from tempest_lib import exceptions as tempest_exceptions
import yaml
from functionaltests.api import base
from functionaltests.api.camp.v1_1 import test_plans
class TestAssembliesController(base.TestCase):
@ -24,6 +28,21 @@ class TestAssembliesController(base.TestCase):
self.client.delete_created_assemblies()
self.client.delete_created_plans()
# TODO(gpilz) - this is a dup of a method in test_plans.TestPlansController
def _create_camp_plan(self, data):
yaml_data = yaml.dump(data)
resp, body = self.client.post('camp/v1_1/plans', yaml_data,
headers={'content-type':
'application/x-yaml'})
plan_resp = base.SolumResponse(resp=resp,
body=body,
body_type='json')
uuid = plan_resp.uuid
if uuid is not None:
# share the Solum client's list of created plans
self.client.created_plans.append(uuid)
return plan_resp
def test_get_solum_assembly(self):
"""Test the CAMP assemblies collection resource.
@ -38,24 +57,102 @@ class TestAssembliesController(base.TestCase):
self.assertEqual(201, p_resp.status)
a_resp = self.client.create_assembly(plan_uuid=p_resp.uuid)
self.assertEqual(201, a_resp.status)
new_uuid = a_resp.uuid
# get the CAMP assemblies resource
# try to get to the newly created assembly through the CAMP assemblies
# resource. it would be more efficient to simply take the UUID of the
# newly created resource and create a CAMP API URI
# (../camp/v1_1/assemblies/<uuid>) from that, but we want to test that
# a link to the Solum-created assembly appears in in the list of links
# in the CAMP plans resource.
resp, body = self.client.get('camp/v1_1/assemblies')
self.assertEqual(200, resp.status, 'GET assemblies resource')
# pick out the assemebly link for our new assembly uuid
assemblies_dct = json.loads(body)
assem_links = assemblies_dct['assembly_links']
self.assertEqual(1, len(assem_links))
camp_link = None
for link in assemblies_dct['assembly_links']:
link_uuid = link['href'].split("/")[-1]
if link_uuid == new_uuid:
camp_link = link
a_link = assem_links[0]
msg = 'Unable to find link to newly created plan in CAMP plans'
self.assertIsNotNone(camp_link, msg)
url = a_link['href'][len(self.client.base_url) + 1:]
url = camp_link['href'][len(self.client.base_url) + 1:]
msg = ("GET Solum assembly resource for %s" %
a_link['target_name'])
camp_link['target_name'])
resp, body = self.client.get(url)
self.assertEqual(200, resp.status, msg)
# right now, this looks like a Solum assembly, not a CAMP
assembly = json.loads(body)
self.assertEqual('assembly', assembly['type'])
self.assertEqual(base.assembly_sample_data['name'], assembly['name'])
def test_create_camp_assembly(self):
"""Test creating a CAMP assembly from a local plan resource.
Creates a plan resource then uses that to create an assembly resource.
"""
if base.config_set_as('camp_enabled', False):
self.skipTest('CAMP not enabled.')
# create a plan using the CAMP API
resp = self._create_camp_plan(data=test_plans.sample_data)
self.assertEqual(resp.status, 201)
uri = (resp.data['uri']
[len(self.client.base_url):])
ref_obj = json.dumps({'plan_uri': uri})
resp, body = self.client.post(
'camp/v1_1/assemblies',
ref_obj,
headers={'content-type': 'application/json'})
self.assertEqual(resp.status, 201)
assem_resp = base.SolumResponse(resp=resp,
body=body,
body_type='json')
uuid = assem_resp.uuid
if uuid is not None:
# share the Solum client's list of created assemblies
self.client.created_assemblies.append(uuid)
def test_delete_plan_with_assemblies(self):
"""Test deleting a plan what has assemblies associated with it.
Creates a plan, an assembly, then tries to delete the plan.
"""
if base.config_set_as('camp_enabled', False):
self.skipTest('CAMP not enabled.')
# create a plan using the CAMP API
resp = self._create_camp_plan(data=test_plans.sample_data)
self.assertEqual(resp.status, 201)
plan_uri = (resp.data['uri']
[len(self.client.base_url):])
ref_obj = json.dumps({'plan_uri': plan_uri})
resp, body = self.client.post(
'camp/v1_1/assemblies',
ref_obj,
headers={'content-type': 'application/json'})
self.assertEqual(resp.status, 201)
assem_resp = base.SolumResponse(resp=resp,
body=body,
body_type='json')
uuid = assem_resp.uuid
if uuid is not None:
# share the Solum client's list of created assemblies
self.client.created_assemblies.append(uuid)
# try to delete the plan before deleting the assembly
# resp, body = self.client.delete(plan_uri[1:])
# self.assertEqual(409, resp.status)
self.assertRaises(tempest_exceptions.Conflict,
self.client.delete, plan_uri[1:])

View File

@ -90,11 +90,12 @@ class TestPlansController(base.TestCase):
new_plan = p_resp.yaml_data
new_uuid = new_plan['uuid']
# try to get to the newly created through the CAMP plans resource. it
# would be more efficient to simply take the UUID of the newly created
# resource and create a CAMP API URI (../camp/v1_1/plans/<uuid>) from
# that, but we want to test that a link to the Solum-created plan
# appears in in the list of links in the CAMP plans resource.
# try to get to the newly plan created through the CAMP plans
# resource. it would be more efficient to simply take the UUID of the
# newly created resource and create a CAMP API URI
# (../camp/v1_1/plans/<uuid>) from that, but we want to test that a
# link to the Solum-created plan appears in in the list of links in
# the CAMP plans resource.
resp, body = self.client.get('camp/v1_1/plans')
self.assertEqual(200, resp.status, 'GET plans resource')

View File

@ -12,20 +12,34 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import urlparse
import pecan
from pecan import rest
from wsme.rest import json as wsme_json
from wsme import types as wsme_types
import wsmeext.pecan as wsme_pecan
from solum.api.controllers.camp.v1_1.datamodel import assemblies as model
from solum.api.controllers.camp.v1_1 import uris
from solum.api.controllers import common_types
from solum.api.handlers import assembly_handler
from solum.api.handlers.camp import assembly_handler
from solum.api.handlers.camp import plan_handler
from solum.common import exception
class AssembliesController(rest.RestController):
"""CAMP v1.1 assemblies controller."""
@exception.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(None, wsme_types.text, status_code=204)
def delete(self, uuid):
"""Delete this assembly."""
handler = assembly_handler.AssemblyHandler(
pecan.request.security_context)
handler.delete(uuid)
@exception.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(model.Assemblies)
def get(self):
@ -33,9 +47,9 @@ class AssembliesController(rest.RestController):
pdef_uri = uris.DEPLOY_PARAMS_URI % pecan.request.host_url
desc = "Solum CAMP API assemblies collection resource"
handlr = (assembly_handler.
AssemblyHandler(pecan.request.security_context))
asem_objs = handlr.get_all()
handler = (assembly_handler.
AssemblyHandler(pecan.request.security_context))
asem_objs = handler.get_all()
a_links = []
for m in asem_objs:
a_links.append(common_types.Link(href=uris.ASSEM_URI_STR %
@ -59,3 +73,113 @@ class AssembliesController(rest.RestController):
parameter_definitions_uri=pdef_uri)
return res
@exception.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(model.Assembly, wsme_types.text)
def get_one(self, uuid):
"""Return the appropriate CAMP-style assembly resource."""
handler = assembly_handler.AssemblyHandler(
pecan.request.security_context)
return model.Assembly.from_db_model(handler.get(uuid),
pecan.request.host_url)
@exception.wrap_pecan_controller_exception
@pecan.expose('json', content_type='application/json')
def post(self):
"""Create a new application.
There are a number of ways to use this method to create a new
application. See Section 6.11 of the CAMP v1.1 specification
for an explanation of each. Use the Content-Type of request to
determine what the client is trying to do.
"""
if pecan.request.content_type is None:
raise exception.UnsupportedMediaType(
name=pecan.request.content_type,
method='POST')
req_content_type = pecan.request.content_type
# deploying by reference uses a JSON payload
if req_content_type == 'application/json':
payload = pecan.request.body
if not payload or len(payload) < 1:
raise exception.BadRequest(reason='empty request body')
try:
json_ref_doc = json.loads(payload)
except ValueError as excp:
raise exception.BadRequest(reason='JSON object is invalid. '
+ excp.message)
if 'plan_uri' in json_ref_doc:
plan_uri_str = json_ref_doc['plan_uri']
# figure out if the plan uri is relative or absolute
plan_uri = urlparse.urlparse(plan_uri_str)
uri_path = plan_uri.path
if not plan_uri.netloc:
# should be something like "../plans/<uuid>" or
# "/camp/v1_1/plans/<uuid> (include Solum plans)
if (not uri_path.startswith('../plans/') and
not uri_path.startswith('../../../v1/plans/') and
not uri_path.startswith('/camp/v1_1/plans/') and
not uri_path.startswith('/v1/plans/')):
msg = 'plan_uri does not reference a plan resource'
raise exception.BadRequest(reason=msg)
plan_uuid = plan_uri.path.split('/')[-1]
else:
# We have an absolute URI. Try to figure out if it refers
# to a plan on this Solum instance. Note the following code
# does not support URI aliases. A request that contains
# a 'plan_uri' with a network location that is different
# than network location used to make this request but
# which, nevertheless, still refers to this Solum instance
# will experience a false negative. This code will treat
# that plan as if it existed on another CAMP-compliant
# server.
if plan_uri_str.startswith(pecan.request.host_url):
if (not uri_path.startswith('/camp/v1_1/plans/') and
not uri_path.startswith('/v1/plans/')):
msg = 'plan_uri does not reference a plan resource'
raise exception.BadRequest(reason=msg)
plan_uuid = plan_uri.path.split('/')[-1]
else:
# The plan exists on another server.
# TODO(gpilz): support references to plans on other
# servers
raise exception.NotImplemented()
# resolve the local plan by its uuid. this will raise a
# ResourceNotFound exception if there is no plan with
# this uuid
phandler = plan_handler.PlanHandler(
pecan.request.security_context)
plan_obj = phandler.get(plan_uuid)
elif 'pdp_uri' in json_ref_doc:
# TODO(gpilz): support references to PDPs
raise exception.NotImplemented()
else:
# must have either 'plan_uri' or 'pdp_uri'
msg = 'JSON object must contain either plan_uri or pdp_uri'
raise exception.BadRequest(reason=msg)
else:
# TODO(gpilz): support deploying an application by value
raise exception.NotImplemented()
# at this point we expect to have a reference to a plan database object
# for the plan that will be used to create the application
ahandler = assembly_handler.AssemblyHandler(
pecan.request.security_context)
assem_db_obj = ahandler.create_from_plan(plan_obj)
assem_model = model.Assembly.from_db_model(assem_db_obj,
pecan.request.host_url)
pecan.response.status = 201
pecan.response.location = assem_model.uri
return wsme_json.tojson(model.Assembly, assem_model)

View File

@ -12,10 +12,47 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
from wsme import types as wtypes
from solum.api.controllers.camp.v1_1 import uris
from solum.api.controllers import common_types
from solum.api.controllers.v1.datamodel import types as api_types
class Assembly(api_types.Base):
"""CAMP v1.1 assembly resource model."""
components = [common_types.Link]
"""CAMP-defined, not currently used."""
plan_uri = common_types.Uri
"""CAMP-defined, also used by Solum."""
operations_uri = common_types.Uri
"""CAMP-defined, not currently used."""
sensors_uri = common_types.Uri
"""CAMP-defined, not currently used."""
status = wtypes.text
"""Solum extension."""
updated_at = datetime.datetime
"""Solum extension."""
created_at = datetime.datetime
"""Solum extension."""
@classmethod
def from_db_model(cls, m, host_url):
obj = super(Assembly, cls).from_db_model(m, host_url)
obj.plan_uri = uris.PLAN_URI_STR % (host_url, m.plan_uuid)
obj.uri = uris.ASSEM_URI_STR % (host_url, m.uuid)
return obj
class Assemblies(api_types.Base):
"""CAMP v1.1 assemblies resource model."""

View File

@ -27,7 +27,7 @@ import wsmeext.pecan as wsme_pecan
from solum.api.controllers.camp.v1_1.datamodel import plans as model
from solum.api.controllers.camp.v1_1 import uris
from solum.api.controllers import common_types
from solum.api.handlers import plan_handler as plan_handler
from solum.api.handlers.camp import plan_handler as plan_handler
from solum.common import exception
from solum.common import yamlutils
from solum.openstack.common.gettextutils import _
@ -219,5 +219,8 @@ class PlansController(rest.RestController):
db_obj = handler.create(clean_plan(wjson.tojson(model.Plan,
model_plan)))
plan_dict = fluff_plan(db_obj.refined_content(), db_obj.uuid)
pecan.response.status = 201
return fluff_plan(db_obj.refined_content(), db_obj.uuid)
pecan.response.location = plan_dict['uri']
return plan_dict

View File

@ -12,8 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
# TODO(gilbert.pilz) - change this to point to CAMP-style assembly resource
ASSEM_URI_STR = '%s/v1/assemblies/%s'
ASSEM_URI_STR = '%s/camp/v1_1/assemblies/%s'
ASSEMS_URI_STR = '%s/camp/v1_1/assemblies'

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#
# 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 uuid
from solum.api.handlers import assembly_handler as solum_assem_handler
from solum.common import solum_keystoneclient
from solum import objects
class AssemblyHandler(solum_assem_handler.AssemblyHandler):
def create_from_plan(self, plan_obj):
"""Create an application using a plan resource as a template."""
db_obj = objects.registry.Assembly()
db_obj.uuid = str(uuid.uuid4())
db_obj.user_id = self.context.user
db_obj.project_id = self.context.tenant
db_obj.trigger_id = str(uuid.uuid4())
db_obj.username = self.context.user_name
# create the trust_id and store it.
ksc = solum_keystoneclient.KeystoneClientV3(self.context)
trust_context = ksc.create_trust_context()
db_obj.trust_id = trust_context.trust_id
# use the plan name as the name of this application
db_obj.name = plan_obj.name + "_application"
db_obj.plan_id = plan_obj.id
db_obj.plan_uuid = plan_obj.uuid
db_obj.status = solum_assem_handler.ASSEMBLY_STATES.QUEUED
db_obj.create(self.context)
artifacts = plan_obj.raw_content.get('artifacts', [])
# build each artifact in the plan
for arti in artifacts:
self._build_artifact(assem=db_obj, artifact=arti)
return db_obj

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
# 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 solum.api.handlers import plan_handler as solum_plan_handler
from solum import objects
class PlanHandler(solum_plan_handler.PlanHandler):
def delete(self, id):
"""Override to simply delete the appropriate row in the DB.
This will raise an exception if any assemblies refer to this plan.
"""
db_obj = objects.registry.Plan.get_by_uuid(self.context, id)
self._delete_params(db_obj.id)
db_obj.destroy(self.context)

View File

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import mock
from solum.api.controllers.camp.v1_1 import assemblies
@ -22,22 +24,125 @@ from solum.tests import fakes
@mock.patch('pecan.request', new_callable=fakes.FakePecanRequest)
@mock.patch('pecan.response', new_callable=fakes.FakePecanResponse)
@mock.patch('solum.api.handlers.assembly_handler.AssemblyHandler')
@mock.patch('solum.api.handlers.camp.assembly_handler.AssemblyHandler')
@mock.patch('solum.api.handlers.camp.plan_handler.PlanHandler')
class TestAssemblies(base.BaseTestCase):
def setUp(self):
super(TestAssemblies, self).setUp()
objects.load()
def test_assemblies_get(self, AssemblyHandler, resp_mock, request_mock):
def test_assemblies_get(self, PlanHandler, AssemblyHandler, resp_mock,
request_mock):
hand_get_all = AssemblyHandler.return_value.get_all
fake_assembly = fakes.FakeAssembly()
hand_get_all.return_value = [fake_assembly]
cont = assemblies.AssembliesController()
resp = cont.get()
resp = assemblies.AssembliesController().get()
self.assertIsNotNone(resp)
self.assertEqual(200, resp_mock.status)
self.assertIsNotNone(resp['result'].assembly_links)
assembly_links = resp['result'].assembly_links
self.assertEqual(1, len(assembly_links))
self.assertEqual(fake_assembly.name, assembly_links[0].target_name)
def test_assemblies_post_no_content_type(self, PlanHandler,
AssemblyHandler, resp_mock,
request_mock):
# creating an assembly requires a Content-Type so the CAMP impl
# can figure out which mechanism the user wants
request_mock.content_type = None
assemblies.AssembliesController().post()
self.assertEqual(415, resp_mock.status)
def test_assemblies_post_ref_none(self, PlanHandler, AssemblyHandler,
resp_mock, request_mock):
# a Content-Type of 'application/json' indicates that the user is
# providing a JSON object that references either a plan or a PDP
request_mock.content_type = 'application/json'
request_mock.body = None
assemblies.AssembliesController().post()
self.assertEqual(400, resp_mock.status)
def test_assemblies_post_ref_empty_json(self, PlanHandler, AssemblyHandler,
resp_mock, request_mock):
# a Content-Type of 'application/json' indicates that the user is
# providing a JSON object that references either a plan or a PDP
request_mock.content_type = 'application/json'
request_mock.body = '{}'
assemblies.AssembliesController().post()
self.assertEqual(400, resp_mock.status)
def test_assemblies_post_ref_bad_rel_uri(self, PlanHandler,
AssemblyHandler, resp_mock,
request_mock):
# a Content-Type of 'application/json' indicates that the user is
# providing a JSON object that references either a plan or a PDP
ref_object = {'plan_uri':
'../fooble/24e3974c-195d-4a6a-96b0-7924ed3c742a'}
request_mock.content_type = 'application/json'
request_mock.body = json.dumps(ref_object)
assemblies.AssembliesController().post()
self.assertEqual(400, resp_mock.status)
def test_assemblies_post_ref_rel_uris(self, PlanHandler, AssemblyHandler,
resp_mock, request_mock):
hand_get = PlanHandler.return_value.get
hand_get.return_value = fakes.FakePlan()
hand_create_from_plan = AssemblyHandler.return_value.create_from_plan
fake_assembly = fakes.FakeAssembly()
hand_create_from_plan.return_value = fake_assembly
cntrl = assemblies.AssembliesController()
# test reference to CAMP plan relative to assemblies
ref_object = {'plan_uri':
'../plans/24e3974c-195d-4a6a-96b0-7924ed3c742a'}
request_mock.content_type = 'application/json'
request_mock.body = json.dumps(ref_object)
resp = cntrl.post()
self.assertIsNotNone(resp)
self.assertEqual(201, resp_mock.status)
self.assertIsNotNone(resp_mock.location)
self.assertEqual(fake_assembly.name, resp['name'])
# test reference to Solum plan relative to assemblies
ref_object = {'plan_uri':
'../../../v1/plans/24e3974c-195d-4a6a-96b0-7924ed3c742a'}
request_mock.content_type = 'application/json'
request_mock.body = json.dumps(ref_object)
resp = cntrl.post()
self.assertIsNotNone(resp)
self.assertEqual(201, resp_mock.status)
self.assertIsNotNone(resp_mock.location)
self.assertEqual(fake_assembly.name, resp['name'])
# test reference to CAMP plan relative to root
ref_object = {'plan_uri':
'/camp/v1_1/plans/24e3974c-195d-4a6a-96b0-7924ed3c742a'}
request_mock.content_type = 'application/json'
request_mock.body = json.dumps(ref_object)
resp = cntrl.post()
self.assertIsNotNone(resp)
self.assertEqual(201, resp_mock.status)
self.assertIsNotNone(resp_mock.location)
self.assertEqual(fake_assembly.name, resp['name'])
# test reference to Solum plan relative to root
ref_object = {'plan_uri':
'/v1/plans/24e3974c-195d-4a6a-96b0-7924ed3c742a'}
request_mock.content_type = 'application/json'
request_mock.body = json.dumps(ref_object)
resp = cntrl.post()
self.assertIsNotNone(resp)
self.assertEqual(201, resp_mock.status)
self.assertIsNotNone(resp_mock.location)
self.assertEqual(fake_assembly.name, resp['name'])

View File

@ -22,7 +22,7 @@ from solum.tests import fakes
@mock.patch('pecan.request', new_callable=fakes.FakePecanRequest)
@mock.patch('pecan.response', new_callable=fakes.FakePecanResponse)
@mock.patch('solum.api.handlers.plan_handler.PlanHandler')
@mock.patch('solum.api.handlers.camp.plan_handler.PlanHandler')
class TestPlans(base.BaseTestCase):
def setUp(self):
super(TestPlans, self).setUp()
@ -33,8 +33,7 @@ class TestPlans(base.BaseTestCase):
fake_plan = fakes.FakePlan()
hand_get_all.return_value = [fake_plan]
cont = plans.PlansController()
resp = cont.get()
resp = plans.PlansController().get()
self.assertIsNotNone(resp)
self.assertEqual(200, resp_mock.status)
self.assertIsNotNone(resp['result'].plan_links)