Merge "Support linking storage on compute creation"
This commit is contained in:
commit
6b5ee3f923
|
@ -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])
|
||||
|
|
|
@ -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", {})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue