From e30fdd75f4d2ed37ef3bf4fef34268f5a286edc3 Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Fri, 11 Nov 2016 13:44:17 +0000 Subject: [PATCH] OCCI 1.2 Contextualization Introduces two new mixins defined in the OCCI 1.2 spec for handling contextualization: user_data and ssh_key. Functionality is similar to the already existing OpenStack mixins, which may be deprecated in the future. Partially-Implements: blueprint occi-1-2-core Change-Id: Ibb6624e3e15bf7b9f3ad39786dcefb36b1a9a797 --- ooi/api/compute.py | 56 ++++--- ooi/api/query.py | 9 +- ooi/exception.py | 5 + ooi/occi/infrastructure/contextualization.py | 85 +++++++++++ ooi/tests/fakes.py | 10 ++ .../middleware/test_compute_controller.py | 37 ++++- ooi/tests/unit/controllers/test_compute.py | 142 ++++++++++++++++-- ooi/tests/unit/controllers/test_query.py | 11 +- .../unit/occi/test_occi_infrastructure.py | 19 +++ 9 files changed, 339 insertions(+), 35 deletions(-) create mode 100644 ooi/occi/infrastructure/contextualization.py diff --git a/ooi/api/compute.py b/ooi/api/compute.py index f0d527f..61b28fd 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -21,11 +21,12 @@ import ooi.api.helpers from ooi import exception from ooi.occi.core import collection from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import contextualization from ooi.occi.infrastructure import network from ooi.occi.infrastructure import storage from ooi.occi.infrastructure import storage_link from ooi.occi import validator as occi_validator -from ooi.openstack import contextualization +from ooi.openstack import contextualization as os_contextualization from ooi.openstack import helpers from ooi.openstack import network as os_network from ooi.openstack import templates @@ -148,7 +149,9 @@ class Controller(ooi.api.base.Controller): ], "optional_mixins": [ contextualization.user_data, - contextualization.public_key, + contextualization.ssh_key, + os_contextualization.user_data, + os_contextualization.public_key, ], "optional_links": [ storage.StorageResource.kind, @@ -166,26 +169,45 @@ class Controller(ooi.api.base.Controller): flavor = obj["schemes"][templates.OpenStackResourceTemplate.scheme][0] user_data, key_name, key_data = None, None, None create_key, create_key_tmp = False, False - if contextualization.user_data.scheme in obj["schemes"]: + # TODO(enolfc): deprecate OS Contextualization in the future + # meanwhile, raise 409 conflict if both contextualizations types appear + if (os_contextualization.user_data.scheme in obj["schemes"] and + contextualization.user_data.scheme in obj["schemes"]): + raise exception.OCCIMixinConflict() + + if os_contextualization.user_data.scheme in obj["schemes"]: user_data = attrs.get("org.openstack.compute.user_data") - if contextualization.public_key.scheme in obj["schemes"]: + if contextualization.user_data.scheme in obj["schemes"]: + user_data = attrs.get("occi.compute.user_data") + + if (os_contextualization.public_key.scheme in obj["schemes"] and + contextualization.ssh_key.scheme in obj["schemes"]): + raise exception.OCCIMixinConflict() + + key_name = key_data = None + if os_contextualization.public_key.scheme in obj["schemes"]: key_name = attrs.get("org.openstack.credentials.publickey.name") key_data = attrs.get("org.openstack.credentials.publickey.data") - if key_name and key_data: - create_key = True - elif not key_name and key_data: - # NOTE(orviz) To be occi-os compliant, not - # raise exception.MissingKeypairName - key_name = uuid.uuid4().hex - create_key = True - create_key_tmp = True + if contextualization.ssh_key.scheme in obj["schemes"]: + if key_data or key_name: + raise exception.OCCIMixinConflict() + key_data = attrs.get("occi.credentials.ssh_key") - if create_key: - # add keypair: if key_name already exists, a 409 HTTP code - # will be returned by OpenStack - self.os_helper.keypair_create(req, key_name, - public_key=key_data) + if key_name and key_data: + create_key = True + elif not key_name and key_data: + # NOTE(orviz) To be occi-os compliant, not + # raise exception.MissingKeypairName + key_name = uuid.uuid4().hex + create_key = True + create_key_tmp = True + + if create_key: + # add keypair: if key_name already exists, a 409 HTTP code + # will be returned by OpenStack + self.os_helper.keypair_create(req, key_name, + public_key=key_data) block_device_mapping_v2 = self._build_block_mapping(req, obj) networks = self._get_network_from_req(req, obj) diff --git a/ooi/api/query.py b/ooi/api/query.py index 23addb7..8d67658 100644 --- a/ooi/api/query.py +++ b/ooi/api/query.py @@ -20,12 +20,13 @@ from ooi.occi.core import entity from ooi.occi.core import link from ooi.occi.core import resource from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import contextualization from ooi.occi.infrastructure import network from ooi.occi.infrastructure import network_link from ooi.occi.infrastructure import storage from ooi.occi.infrastructure import storage_link from ooi.occi.infrastructure import templates as infra_templates -from ooi.openstack import contextualization +from ooi.openstack import contextualization as os_contextualization from ooi.openstack import network as os_network from ooi.openstack import templates @@ -108,8 +109,12 @@ class Controller(base.Controller): mixins.extend(self._os_tpls(req)) # OpenStack Contextualization + mixins.append(os_contextualization.user_data) + mixins.append(os_contextualization.public_key) + + # OCCI Contextualization mixins.append(contextualization.user_data) - mixins.append(contextualization.public_key) + mixins.append(contextualization.ssh_key) # OpenStack Floating IP Pools mixins.extend(self._ip_pools(req)) diff --git a/ooi/exception.py b/ooi/exception.py index 467386d..06793d2 100644 --- a/ooi/exception.py +++ b/ooi/exception.py @@ -147,3 +147,8 @@ class NetworkPoolFound(NotFound): class MissingKeypairName(Invalid): msg_fmt = "Missing Keypair Name" code = 400 + + +class OCCIMixinConflict(OCCIException): + msg_fmt = "Conflicting OCCI Mixins used in request" + code = 409 diff --git a/ooi/occi/infrastructure/contextualization.py b/ooi/occi/infrastructure/contextualization.py new file mode 100644 index 0000000..29078de --- /dev/null +++ b/ooi/occi/infrastructure/contextualization.py @@ -0,0 +1,85 @@ +# Copyright 2015 Spanish National Research Council +# +# 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 ooi.occi.core import attribute +from ooi.occi.core import mixin +from ooi.occi import helpers +from ooi.occi.infrastructure import compute + + +class SSHKey(mixin.Mixin): + scheme = helpers.build_scheme("infrastructure/credentials") + term = "ssh_key" + + def __init__(self, ssh_key=None): + attrs = [ + attribute.MutableAttribute( + "occi.crendentials.ssh.publickey", ssh_key, required=True, + description=("The contents of the public key file to be " + "injected into the Compute Resource"), + attr_type=attribute.AttributeType.string_type), + ] + + attrs = attribute.AttributeCollection({a.name: a for a in attrs}) + + super(SSHKey, self).__init__(SSHKey.scheme, SSHKey.term, + "Credentials mixin", + attributes=attrs, + applies=[compute.ComputeResource.kind]) + + @property + def ssh_key(self): + attr = "occi.crendentials.ssh.publickey" + return self.attributes[attr].value + + @ssh_key.setter + def ssh_key(self, value): + attr = "occi.crendentials.ssh.publickey" + self.attributes[attr].value = value + + +class UserData(mixin.Mixin): + scheme = helpers.build_scheme("infrastructure/compute") + term = "user_data" + + def __init__(self, user_data=None): + attrs = [ + attribute.MutableAttribute( + "occi.compute.userdata", user_data, required=True, + description=("Contextualization data (e.g., script, " + "executable) that the client supplies once and " + "only once. It cannot be updated."), + attr_type=attribute.AttributeType.string_type), + ] + + attrs = attribute.AttributeCollection({a.name: a for a in attrs}) + + super(UserData, self).__init__(UserData.scheme, + UserData.term, + "Contextualization mixin", + attributes=attrs, + applies=[compute.ComputeResource.kind]) + + @property + def user_data(self): + attr = "occi.compute.userdata" + return self.attributes[attr].value + + @user_data.setter + def user_data(self, value): + attr = "occi.compute.userdata" + self.attributes[attr].value = value + +ssh_key = SSHKey() +user_data = UserData() diff --git a/ooi/tests/fakes.py b/ooi/tests/fakes.py index 1e767d3..58205f4 100644 --- a/ooi/tests/fakes.py +++ b/ooi/tests/fakes.py @@ -409,6 +409,16 @@ def fake_query_results(): 'scheme="http://schemas.openstack.org/instance/credentials#"; ' 'class="mixin"; title="Contextualization extension - public_key"') + # OCCI contextualization + cats.append( + 'user_data; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/compute#"; ' + 'class="mixin"; title="Contextualization mixin"') + cats.append( + 'ssh_key; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/credentials#"; ' + 'class="mixin"; title="Credentials mixin"') + result = [] for c in cats: result.append(("Category", c)) diff --git a/ooi/tests/functional/middleware/test_compute_controller.py b/ooi/tests/functional/middleware/test_compute_controller.py index 607e9f1..8b72e40 100644 --- a/ooi/tests/functional/middleware/test_compute_controller.py +++ b/ooi/tests/functional/middleware/test_compute_controller.py @@ -257,7 +257,7 @@ class TestComputeController(test_middleware.TestMiddleware): self.assertEqual(400, resp.status_code) self.assertDefaults(resp) - def test_create_with_context(self): + def test_create_with_os_context(self): tenant = fakes.tenants["foo"] app = self.get_app() @@ -292,6 +292,41 @@ class TestComputeController(test_middleware.TestMiddleware): self.assertExpectedResult(expected, resp) self.assertDefaults(resp) + def test_create_with_occi_context(self): + tenant = fakes.tenants["foo"] + + 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",' + 'user_data;' + 'scheme="http://schemas.ogf.org/occi/infrastructure/compute#";' + 'class="mixin"' + ), + 'X-OCCI-Attribute': ( + 'occi.compute.user_data="foo"' + ) + } + + 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_create_vm_with_storage(self): tenant = fakes.tenants["foo"] target = utils.join_url(self.application_url + "/", diff --git a/ooi/tests/unit/controllers/test_compute.py b/ooi/tests/unit/controllers/test_compute.py index b242027..dba009a 100644 --- a/ooi/tests/unit/controllers/test_compute.py +++ b/ooi/tests/unit/controllers/test_compute.py @@ -24,9 +24,10 @@ 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 contextualization from ooi.occi.infrastructure import network as occi_network from ooi.occi.infrastructure import storage as occi_storage -from ooi.openstack import contextualization +from ooi.openstack import contextualization as os_contextualization from ooi.openstack import templates from ooi.tests import base from ooi.tests import fakes @@ -272,8 +273,8 @@ class TestComputeController(base.TestController): @mock.patch.object(helpers.OpenStackHelper, "create_server") @mock.patch.object(compute.Controller, "_get_network_from_req") @mock.patch("ooi.occi.validator.Validator") - def test_create_server_with_context(self, m_validator, m_net, - m_create): + def test_create_server_with_os_context(self, m_validator, m_net, + m_create): tenant = fakes.tenants["foo"] req = self._build_req(tenant["id"]) obj = { @@ -284,8 +285,7 @@ class TestComputeController(base.TestController): "schemes": { templates.OpenStackOSTemplate.scheme: ["foo"], templates.OpenStackResourceTemplate.scheme: ["bar"], - contextualization.user_data.scheme: None, - contextualization.public_key.scheme: None, + os_contextualization.user_data.scheme: None, }, } # NOTE(aloga): the mocked call is @@ -306,12 +306,69 @@ class TestComputeController(base.TestController): block_device_mapping_v2=[], networks=net) + @mock.patch.object(helpers.OpenStackHelper, "create_server") + @mock.patch.object(compute.Controller, "_get_network_from_req") + @mock.patch("ooi.occi.validator.Validator") + def test_create_server_with_occi_context(self, m_validator, m_net, + m_create): + tenant = fakes.tenants["foo"] + req = self._build_req(tenant["id"]) + obj = { + "attributes": { + "occi.core.title": "foo instance", + "occi.compute.user_data": "bazonk", + }, + "schemes": { + templates.OpenStackOSTemplate.scheme: ["foo"], + templates.OpenStackResourceTemplate.scheme: ["bar"], + contextualization.user_data.scheme: None, + }, + } + # NOTE(aloga): the mocked call is + # "parser = req.get_parser()(req.headers, req.body)" + req.get_parser = mock.MagicMock() + # NOTE(aloga): MOG! + req.get_parser.return_value.return_value.parse.return_value = obj + m_validator.validate.return_value = True + server = {"id": uuid.uuid4().hex} + m_create.return_value = server + net = [{'uuid': uuid.uuid4().hex}] + m_net.return_value = net + ret = self.controller.create(req, None) # noqa + self.assertIsInstance(ret, collection.Collection) + m_create.assert_called_with(mock.ANY, "foo instance", "foo", "bar", + user_data="bazonk", + key_name=None, + block_device_mapping_v2=[], + networks=net) + + @mock.patch("ooi.occi.validator.Validator") + def test_create_server_with_context_conflict(self, m_validator): + 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"], + os_contextualization.user_data.scheme: None, + contextualization.user_data.scheme: None, + }, + } + req.get_parser = mock.MagicMock() + req.get_parser.return_value.return_value.parse.return_value = obj + m_validator.validate.return_value = True + self.assertRaises(exception.OCCIMixinConflict, self.controller.create, + req, None) + @mock.patch.object(helpers.OpenStackHelper, "keypair_create") @mock.patch.object(helpers.OpenStackHelper, "create_server") @mock.patch.object(compute.Controller, "_get_network_from_req") @mock.patch("ooi.occi.validator.Validator") - def test_create_server_with_sshkeys(self, m_validator, m_net, - m_server, m_keypair): + def test_create_server_with_os_sshkey(self, m_validator, m_net, + m_server, m_keypair): tenant = fakes.tenants["foo"] req = self._build_req(tenant["id"]) obj = { @@ -323,7 +380,7 @@ class TestComputeController(base.TestController): "schemes": { templates.OpenStackOSTemplate.scheme: ["foo"], templates.OpenStackResourceTemplate.scheme: ["bar"], - contextualization.public_key.scheme: None, + os_contextualization.public_key.scheme: None, }, } req.get_parser = mock.MagicMock() @@ -349,9 +406,9 @@ class TestComputeController(base.TestController): @mock.patch.object(helpers.OpenStackHelper, "create_server") @mock.patch.object(compute.Controller, "_get_network_from_req") @mock.patch("ooi.occi.validator.Validator") - def test_create_server_with_no_sshkey_name(self, m_validator, m_net, - m_server, m_keypair, - m_keypair_delete): + def test_create_server_with_os_sshkey_no_name(self, m_validator, m_net, + m_server, m_keypair, + m_keypair_delete): tenant = fakes.tenants["foo"] req = self._build_req(tenant["id"]) obj = { @@ -362,7 +419,7 @@ class TestComputeController(base.TestController): "schemes": { templates.OpenStackOSTemplate.scheme: ["foo"], templates.OpenStackResourceTemplate.scheme: ["bar"], - contextualization.public_key.scheme: None, + os_contextualization.public_key.scheme: None, }, } req.get_parser = mock.MagicMock() @@ -384,6 +441,67 @@ class TestComputeController(base.TestController): networks=net) m_keypair_delete.assert_called_with(mock.ANY, mock.ANY) + @mock.patch.object(helpers.OpenStackHelper, "keypair_delete") + @mock.patch.object(helpers.OpenStackHelper, "keypair_create") + @mock.patch.object(helpers.OpenStackHelper, "create_server") + @mock.patch.object(compute.Controller, "_get_network_from_req") + @mock.patch("ooi.occi.validator.Validator") + def test_create_server_with_occi_sshkey(self, m_validator, m_net, + m_server, m_keypair, + m_keypair_delete): + tenant = fakes.tenants["foo"] + req = self._build_req(tenant["id"]) + obj = { + "attributes": { + "occi.core.title": "foo instance", + "occi.credentials.ssh_key": "wtfoodata" + }, + "schemes": { + templates.OpenStackOSTemplate.scheme: ["foo"], + templates.OpenStackResourceTemplate.scheme: ["bar"], + contextualization.ssh_key.scheme: None, + }, + } + 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_keypair.return_value = None + net = [{'uuid': uuid.uuid4().hex}] + m_net.return_value = net + ret = self.controller.create(req, None) # noqa + self.assertIsInstance(ret, collection.Collection) + m_keypair.assert_called_with(mock.ANY, mock.ANY, + public_key="wtfoodata") + m_server.assert_called_with(mock.ANY, "foo instance", "foo", "bar", + user_data=None, + key_name=mock.ANY, + block_device_mapping_v2=[], + networks=net) + m_keypair_delete.assert_called_with(mock.ANY, mock.ANY) + + @mock.patch("ooi.occi.validator.Validator") + def test_create_server_with_sshkey_conflict(self, m_validator): + 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"], + os_contextualization.public_key.scheme: None, + contextualization.ssh_key.scheme: None, + }, + } + req.get_parser = mock.MagicMock() + req.get_parser.return_value.return_value.parse.return_value = obj + m_validator.validate.return_value = True + self.assertRaises(exception.OCCIMixinConflict, self.controller.create, + req, None) + def test_build_block_mapping_no_links(self): ret = self.controller._build_block_mapping(None, {}) self.assertEqual([], ret) diff --git a/ooi/tests/unit/controllers/test_query.py b/ooi/tests/unit/controllers/test_query.py index 6332120..fcbeb65 100644 --- a/ooi/tests/unit/controllers/test_query.py +++ b/ooi/tests/unit/controllers/test_query.py @@ -20,12 +20,13 @@ from ooi.occi.core import entity from ooi.occi.core import link from ooi.occi.core import resource from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import contextualization from ooi.occi.infrastructure import network from ooi.occi.infrastructure import network_link from ooi.occi.infrastructure import storage from ooi.occi.infrastructure import storage_link from ooi.occi.infrastructure import templates as infra_templates -from ooi.openstack import contextualization +from ooi.openstack import contextualization as os_contextualization from ooi.openstack import network as os_network from ooi.openstack import templates from ooi.tests import base @@ -74,8 +75,10 @@ class TestQueryController(base.TestController): network_link.ip_network_interface, infra_templates.os_tpl, infra_templates.resource_tpl, + os_contextualization.user_data, + os_contextualization.public_key, contextualization.user_data, - contextualization.public_key, + contextualization.ssh_key, ] expected_actions = [ @@ -141,8 +144,10 @@ class TestQueryController(base.TestController): network_link.ip_network_interface, infra_templates.os_tpl, infra_templates.resource_tpl, + os_contextualization.user_data, + os_contextualization.public_key, contextualization.user_data, - contextualization.public_key, + contextualization.ssh_key, ] expected_actions = [ diff --git a/ooi/tests/unit/occi/test_occi_infrastructure.py b/ooi/tests/unit/occi/test_occi_infrastructure.py index 3f273c4..90eba4a 100644 --- a/ooi/tests/unit/occi/test_occi_infrastructure.py +++ b/ooi/tests/unit/occi/test_occi_infrastructure.py @@ -18,6 +18,7 @@ from ooi.occi.core import link from ooi.occi.core import mixin from ooi.occi.core import resource from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import contextualization from ooi.occi.infrastructure import network from ooi.occi.infrastructure import network_link from ooi.occi.infrastructure import storage @@ -340,3 +341,21 @@ class TestOCCINetworkInterface(base.TestCase): self.assertEqual("00:01:02:03:04:05", l.mac) self.assertEqual("foo", l.state) self.assertEqual("msg", l.message) + + +class TestOCCIUserData(base.TestCase): + def test_occi_userdata(self): + user_data = "foobar" + mxn = contextualization.UserData(user_data) + self.assertEqual("user_data", mxn.term) + self.assertEqual(user_data, mxn.user_data) + self.assertEqual([compute.ComputeResource.kind], mxn.applies) + + +class TestOCCISSHKey(base.TestCase): + def test_occi_ssh_key(self): + key_data = "1234" + mxn = contextualization.SSHKey(key_data) + self.assertEqual("ssh_key", mxn.term) + self.assertEqual(key_data, mxn.ssh_key) + self.assertEqual([compute.ComputeResource.kind], mxn.applies)