Merge "Support linking storage on compute creation"

This commit is contained in:
Jenkins 2015-12-22 21:08:50 +00:00 committed by Gerrit Code Review
commit 6b5ee3f923
9 changed files with 321 additions and 23 deletions

View File

@ -102,6 +102,36 @@ class Controller(ooi.api.base.Controller):
self.os_helper.run_action(req, action, id)
return []
def _build_block_mapping(self, req, obj):
mappings = []
for l in obj.get("links", {}).values():
if l["rel"] == storage.StorageResource.kind.type_id:
_, vol_id = ooi.api.helpers.get_id_with_kind(
req,
l.get("occi.core.target"),
storage.StorageResource.kind)
mapping = {
"source_type": "volume",
"uuid": vol_id,
"delete_on_termination": False,
}
try:
mapping['device_name'] = l['occi.storagelink.deviceid']
except KeyError:
pass
mappings.append(mapping)
# this needs to be there if we have a mapping
if mappings:
image = obj["schemes"][templates.OpenStackOSTemplate.scheme][0]
mappings.insert(0, {
"source_type": "image",
"destination_type": "local",
"boot_index": 0,
"delete_on_termination": True,
"uuid": image,
})
return mappings
def create(self, req, body):
parser = req.get_parser()(req.headers, req.body)
scheme = {
@ -113,7 +143,11 @@ class Controller(ooi.api.base.Controller):
"optional_mixins": [
contextualization.user_data,
contextualization.public_key,
],
"optional_links": [
storage.StorageResource.kind,
]
}
obj = parser.parse()
validator = occi_validator.Validator(obj)
@ -146,9 +180,16 @@ class Controller(ooi.api.base.Controller):
self.os_helper.keypair_create(req, key_name,
public_key=key_data)
server = self.os_helper.create_server(req, name, image, flavor,
user_data=user_data,
key_name=key_name)
block_device_mapping_v2 = self._build_block_mapping(req, obj)
server = self.os_helper.create_server(
req,
name,
image,
flavor,
user_data=user_data,
key_name=key_name,
block_device_mapping_v2=block_device_mapping_v2)
# The returned JSON does not contain the server name
server["name"] = name
occi_compute_resources = self._get_compute_resources([server])

View File

@ -244,7 +244,8 @@ class OpenStackHelper(BaseHelper):
def _get_create_server_req(self, req, name, image, flavor,
user_data=None,
key_name=None):
key_name=None,
block_device_mapping_v2=None):
tenant_id = self.tenant_from_req(req)
path = "/%s/servers" % tenant_id
# TODO(enolfc): add here the correct metadata info
@ -260,6 +261,8 @@ class OpenStackHelper(BaseHelper):
body["server"]["user_data"] = user_data
if key_name is not None:
body["server"]["key_name"] = key_name
if block_device_mapping_v2:
body["server"]["block_device_mapping_v2"] = block_device_mapping_v2
return self._get_req(req,
path=path,
@ -268,7 +271,8 @@ class OpenStackHelper(BaseHelper):
method="POST")
def create_server(self, req, name, image, flavor,
user_data=None, key_name=None):
user_data=None, key_name=None,
block_device_mapping_v2=None):
"""Create a server.
:param req: the incoming request
@ -278,9 +282,14 @@ class OpenStackHelper(BaseHelper):
:param user_data: user data to inject into the server
:param key_name: user public key name
"""
req = self._get_create_server_req(req, name, image, flavor,
user_data=user_data,
key_name=key_name)
req = self._get_create_server_req(
req,
name,
image,
flavor,
user_data=user_data,
key_name=key_name,
block_device_mapping_v2=block_device_mapping_v2)
response = req.get_response(self.app)
# We only get one server
return self.get_from_response(response, "server", {})

View File

@ -69,6 +69,20 @@ class Validator(object):
break
return unmatched
def _validate_optional_links(self, expected, links):
for uri, l in links.items():
try:
rel = l['rel']
except KeyError:
raise exception.OCCIMissingType(type_id=uri)
for ex in expected:
if rel == ex.type_id:
break
else:
expected_types = ', '.join([e.type_id for e in expected])
raise exception.OCCISchemaMismatch(expected=expected_types,
found=l['rel'])
def validate(self, schema):
if "category" in schema:
self._validate_category(schema["category"])
@ -81,4 +95,7 @@ class Validator(object):
if unexpected:
raise exception.OCCISchemaMismatch(expected="",
found=unexpected)
self._validate_optional_links(
schema.get("optional_links", []),
self.parsed_obj.get("links", {}))
return True

View File

@ -26,6 +26,7 @@ from ooi.api import helpers
from ooi import exception
from ooi.occi.core import collection
from ooi.occi.infrastructure import compute as occi_compute
from ooi.occi.infrastructure import storage as occi_storage
from ooi.openstack import contextualization
from ooi.openstack import templates
from ooi.tests import base
@ -253,7 +254,8 @@ class TestComputeController(base.TestController):
self.assertIsInstance(ret, collection.Collection)
m_create.assert_called_with(mock.ANY, "foo instance", "foo", "bar",
user_data=None,
key_name=None)
key_name=None,
block_device_mapping_v2=[])
@mock.patch.object(helpers.OpenStackHelper, "create_server")
@mock.patch("ooi.occi.validator.Validator")
@ -284,7 +286,8 @@ class TestComputeController(base.TestController):
self.assertIsInstance(ret, collection.Collection)
m_create.assert_called_with(mock.ANY, "foo instance", "foo", "bar",
user_data="bazonk",
key_name=None)
key_name=None,
block_device_mapping_v2=[])
@mock.patch.object(helpers.OpenStackHelper, "keypair_create")
@mock.patch.object(helpers.OpenStackHelper, "create_server")
@ -317,7 +320,8 @@ class TestComputeController(base.TestController):
public_key="wtfoodata")
m_server.assert_called_with(mock.ANY, "foo instance", "foo", "bar",
user_data=None,
key_name="wtfoo")
key_name="wtfoo",
block_device_mapping_v2=[])
@mock.patch.object(helpers.OpenStackHelper, "keypair_delete")
@mock.patch.object(helpers.OpenStackHelper, "keypair_create")
@ -350,5 +354,119 @@ class TestComputeController(base.TestController):
public_key="wtfoodata")
m_server.assert_called_with(mock.ANY, "foo instance", "foo", "bar",
user_data=None,
key_name=mock.ANY)
key_name=mock.ANY,
block_device_mapping_v2=[])
m_keypair_delete.assert_called_with(mock.ANY, mock.ANY)
def test_build_block_mapping_no_links(self):
ret = self.controller._build_block_mapping(None, {})
self.assertEqual([], ret)
def test_build_block_mapping_invalid_rel(self):
obj = {'links': {'foo': {'rel': 'bar'}}}
ret = self.controller._build_block_mapping(None, obj)
self.assertEqual([], ret)
@mock.patch("ooi.api.helpers.get_id_with_kind")
def test_build_block_mapping(self, m_get_id):
vol_id = uuid.uuid4().hex
image_id = uuid.uuid4().hex
obj = {
'links': {
'l1': {
'rel': ('http://schemas.ogf.org/occi/infrastructure#'
'storage'),
'occi.core.target': vol_id,
}
},
"schemes": {
templates.OpenStackOSTemplate.scheme: [image_id],
}
}
m_get_id.return_value = (None, vol_id)
ret = self.controller._build_block_mapping(None, obj)
expected = [
{
"source_type": "image",
"destination_type": "local",
"boot_index": 0,
"delete_on_termination": True,
"uuid": image_id,
},
{
"source_type": "volume",
"uuid": vol_id,
"delete_on_termination": False,
}
]
self.assertEqual(expected, ret)
m_get_id.assert_called_with(None, vol_id,
occi_storage.StorageResource.kind)
@mock.patch("ooi.api.helpers.get_id_with_kind")
def test_build_block_mapping_device_id(self, m_get_id):
vol_id = uuid.uuid4().hex
image_id = uuid.uuid4().hex
obj = {
'links': {
'l1': {
'rel': ('http://schemas.ogf.org/occi/infrastructure#'
'storage'),
'occi.core.target': vol_id,
'occi.storagelink.deviceid': 'baz'
}
},
"schemes": {
templates.OpenStackOSTemplate.scheme: [image_id],
}
}
m_get_id.return_value = (None, vol_id)
ret = self.controller._build_block_mapping(None, obj)
expected = [
{
"source_type": "image",
"destination_type": "local",
"boot_index": 0,
"delete_on_termination": True,
"uuid": image_id,
},
{
"source_type": "volume",
"uuid": vol_id,
"delete_on_termination": False,
"device_name": "baz",
}
]
self.assertEqual(expected, ret)
m_get_id.assert_called_with(None, vol_id,
occi_storage.StorageResource.kind)
@mock.patch.object(helpers.OpenStackHelper, "create_server")
@mock.patch.object(compute.Controller, "_build_block_mapping")
@mock.patch("ooi.occi.validator.Validator")
def test_create_server_with_storage_link(self, m_validator, m_block,
m_server):
tenant = fakes.tenants["foo"]
req = self._build_req(tenant["id"])
obj = {
"attributes": {
"occi.core.title": "foo instance",
},
"schemes": {
templates.OpenStackOSTemplate.scheme: ["foo"],
templates.OpenStackResourceTemplate.scheme: ["bar"],
},
}
req.get_parser = mock.MagicMock()
req.get_parser.return_value.return_value.parse.return_value = obj
m_validator.validate.return_value = True
server = {"id": uuid.uuid4().hex}
m_server.return_value = server
m_block.return_value = "mapping"
ret = self.controller.create(req, None) # noqa
self.assertIsInstance(ret, collection.Collection)
m_server.assert_called_with(mock.ANY, "foo instance", "foo", "bar",
user_data=None,
key_name=mock.ANY,
block_device_mapping_v2="mapping")
m_block.assert_called_with(req, obj)

View File

@ -615,12 +615,14 @@ class TestOpenStackHelper(TestBaseHelper):
flavor = uuid.uuid4().hex
user_data = "foo"
key_name = "wtfoo"
bdm = []
ret = self.helper.create_server(None, name, image, flavor,
user_data=user_data,
key_name=key_name)
key_name=key_name,
block_device_mapping_v2=bdm)
self.assertEqual("FOO", ret)
m.assert_called_with(None, name, image, flavor, user_data=user_data,
key_name=key_name)
key_name=key_name, block_device_mapping_v2=bdm)
@mock.patch("ooi.api.helpers.exception_from_response")
@mock.patch.object(helpers.OpenStackHelper, "_get_create_server_req")
@ -635,6 +637,7 @@ class TestOpenStackHelper(TestBaseHelper):
flavor = uuid.uuid4().hex
user_data = "foo"
key_name = "wtfoo"
bdm = []
m_exc.return_value = webob.exc.HTTPInternalServerError()
self.assertRaises(webob.exc.HTTPInternalServerError,
self.helper.create_server,
@ -643,9 +646,10 @@ class TestOpenStackHelper(TestBaseHelper):
image,
flavor,
user_data=user_data,
key_name=key_name)
key_name=key_name,
block_device_mapping_v2=bdm)
m.assert_called_with(None, name, image, flavor, user_data=user_data,
key_name=key_name)
key_name=key_name, block_device_mapping_v2=bdm)
m_exc.assert_called_with(resp)
@mock.patch.object(helpers.OpenStackHelper, "_get_volume_create_req")

View File

@ -294,6 +294,38 @@ class TestComputeController(test_middleware.TestMiddleware):
self.assertExpectedResult(expected, resp)
self.assertDefaults(resp)
def test_create_vm_with_storage(self):
tenant = fakes.tenants["foo"]
target = utils.join_url(self.application_url + "/",
"storage/%s" % uuid.uuid4().hex)
app = self.get_app()
headers = {
'Category': (
'compute;'
'scheme="http://schemas.ogf.org/occi/infrastructure#";'
'class="kind",'
'foo;'
'scheme="http://schemas.openstack.org/template/resource#";'
'class="mixin",'
'bar;'
'scheme="http://schemas.openstack.org/template/os#";'
'class="mixin"'),
'Link': (
'</bar>;'
'rel="http://schemas.ogf.org/occi/infrastructure#storage";'
'occi.core.target="%s"') % target
}
req = self._build_req("/compute", tenant["id"], method="POST",
headers=headers)
resp = req.get_response(app)
expected = [("X-OCCI-Location",
utils.join_url(self.application_url + "/",
"compute/%s" % "foo"))]
self.assertEqual(200, resp.status_code)
self.assertExpectedResult(expected, resp)
self.assertDefaults(resp)
def test_vm_links(self):
tenant = fakes.tenants["baz"]

View File

@ -102,6 +102,29 @@ class TestParserBase(base.TestCase):
expected_attrs = {"foo": "bar", "baz": "1234", "bazonk": "foo=123"}
self.assertEqual(expected_attrs, res["attributes"])
def test_link(self):
headers = {
'Category': ('foo; '
'scheme="http://example.com/scheme#"; '
'class="kind"'),
'Link': ('<bar>; foo="bar"; "bazonk"="foo=123"')
}
parser = self._get_parser(headers, None)
res = parser.parse()
expected_links = {"bar": {"foo": "bar", "bazonk": "foo=123"}}
self.assertEqual(expected_links, res["links"])
def test_invalid_link(self):
headers = {
'Category': ('foo; '
'scheme="http://example.com/scheme#"; '
'class="kind"'),
'Link': ('bar; foo="bar"; "bazonk"="foo=123"')
}
parser = self._get_parser(headers, None)
self.assertRaises(exception.OCCIInvalidSchema,
parser.parse)
class TestTextParser(TestParserBase):
def _get_parser(self, headers, body):

View File

@ -186,3 +186,37 @@ class TestValidator(base.TestCase):
v = validator.Validator(pobj)
self.assertRaises(exception.OCCISchemaMismatch, v.validate, {})
def test_optional_links(self):
mixins = collections.Counter()
schemes = collections.defaultdict(list)
links = {"foo": {"rel": "http://example.com/scheme#foo"}}
pobj = {
"kind": "compute",
"category": "foo type",
"mixins": mixins,
"schemes": schemes,
"links": links
}
link = mock.MagicMock()
link.type_id = "http://example.com/scheme#foo"
scheme = {"optional_links": [link]}
v = validator.Validator(pobj)
self.assertTrue(v.validate(scheme))
def test_optional_links_invalid(self):
mixins = collections.Counter()
schemes = collections.defaultdict(list)
links = {"foo": {"rel": "http://example.com/scheme#foo"}}
pobj = {
"kind": "compute",
"category": "foo type",
"mixins": mixins,
"schemes": schemes,
"links": links
}
link = mock.MagicMock()
link.type_id = "http://example.com/scheme#foo"
scheme = {"optional_links": []}
v = validator.Validator(pobj)
self.assertRaises(exception.OCCISchemaMismatch, v.validate, scheme)

View File

@ -121,6 +121,25 @@ class TextParser(BaseParser):
pass
return attrs
def parse_links(self, headers):
links = {}
try:
header_links = headers["Link"]
except KeyError:
return links
for link in _quoted_split(header_links):
ll = _quoted_split(link, "; ")
# remove the "<" and ">"
if ll[0][1] != "<" and ll[0][-1] != ">":
raise exception.OCCIInvalidSchema("Unable to parse link")
link_dest = ll[0][1:-1]
try:
d = dict([_split_unquote(i) for i in ll[1:]])
except ValueError:
raise exception.OCCIInvalidSchema("Unable to parse link")
links[link_dest] = d
return links
def _convert_to_headers(self):
if not self.body:
raise exception.OCCIInvalidSchema("No schema found")
@ -130,18 +149,19 @@ class TextParser(BaseParser):
hdrs[hdr].append(content)
return {hdr: ','.join(hdrs[hdr]) for hdr in hdrs}
def parse(self):
body_headers = self._convert_to_headers()
obj = self.parse_categories(body_headers)
obj['attributes'] = self.parse_attributes(body_headers)
def _parse(self, headers):
obj = self.parse_categories(headers)
obj['attributes'] = self.parse_attributes(headers)
obj['links'] = self.parse_links(headers)
return obj
def parse(self):
return self._parse(self._convert_to_headers())
class HeaderParser(TextParser):
def parse(self):
obj = self.parse_categories(self.headers)
obj['attributes'] = self.parse_attributes(self.headers)
return obj
return self._parse(self.headers)
_PARSERS_MAP = {