diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 04099d2..6d898d7 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -12,8 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import os.path import uuid +import six.moves.urllib.parse as urlparse import webob.exc import ooi.api.base @@ -63,6 +65,21 @@ class Controller(ooi.api.base.Controller): return collection.Collection(resources=occi_compute_resources) + def _save_server(self, req, id, server, obj): + attrs = obj.get("attributes", {}) + img_name = attrs.get("name", server.get("name", "")) + action_args = {"name": img_name} + + res = self.os_helper.run_action(req, "save", id, action_args) + + # save action requires that the new image is returned to the user + # this is available in Location header of the createImage reponse + # not documented at http://developer.openstack.org/api-ref/compute + img_url = urlparse.urlparse(res.headers["Location"]) + tpl = templates.OpenStackOSTemplate(os.path.basename(img_url[2]), + img_name) + return collection.Collection(mixins=[tpl]) + def run_action(self, req, id, body): action = req.GET.get("action", None) occi_actions = [a.term for a in compute.ComputeResource.actions] @@ -87,14 +104,19 @@ class Controller(ooi.api.base.Controller): scheme = {"category": compute.restart} elif action == "suspend": scheme = {"category": compute.suspend} + elif action == "save": + scheme = {"category": compute.save} else: raise exception.NotImplemented validator = occi_validator.Validator(obj) validator.validate(scheme) - self.os_helper.run_action(req, action, id) - return [] + if action == "save": + return self._save_server(req, id, server, obj) + else: + self.os_helper.run_action(req, action, id) + return [] def _build_block_mapping(self, req, obj): mappings = [] diff --git a/ooi/api/helpers.py b/ooi/api/helpers.py index 45611df..3e9bae6 100644 --- a/ooi/api/helpers.py +++ b/ooi/api/helpers.py @@ -288,7 +288,7 @@ class OpenStackHelper(BaseHelper): if response.status_int not in [204]: raise exception_from_response(response) - def _get_run_action_req(self, req, action, server_id): + def _get_run_action_req(self, req, action, server_id, action_args=None): tenant_id = self.tenant_from_req(req) path = "/%s/servers/%s/action" % (tenant_id, server_id) @@ -299,23 +299,29 @@ class OpenStackHelper(BaseHelper): "resume": {"resume": None}, "unpause": {"unpause": None}, "restart": {"reboot": {"type": "SOFT"}}, + "save": {"createImage": None} } - action = actions_map[action] + + os_action, default_args = actions_map[action].popitem() + if action_args is None: + action_args = default_args + action = {os_action: action_args} body = json.dumps(action) return self._get_req(req, path=path, body=body, method="POST") - def run_action(self, req, action, server_id): + def run_action(self, req, action, server_id, action_args=None): """Run an action on a server. :param req: the incoming request :param action: the action to run :param server_id: server id to delete """ - os_req = self._get_run_action_req(req, action, server_id) + os_req = self._get_run_action_req(req, action, server_id, action_args) response = os_req.get_response(self.app) if response.status_int != 202: raise exception_from_response(response) + return response def _get_server_req(self, req, server_id): tenant_id = self.tenant_from_req(req) diff --git a/ooi/occi/infrastructure/compute.py b/ooi/occi/infrastructure/compute.py index ba962da..8844e85 100644 --- a/ooi/occi/infrastructure/compute.py +++ b/ooi/occi/infrastructure/compute.py @@ -30,6 +30,9 @@ restart = action.Action(helpers.build_scheme('infrastructure/compute/action'), suspend = action.Action(helpers.build_scheme('infrastructure/compute/action'), "suspend", "suspend compute instance") +save = action.Action(helpers.build_scheme('infrastructure/compute/action'), + "save", "save compute instance") + class ComputeResource(resource.Resource): attributes = attr.AttributeCollection({ @@ -63,7 +66,7 @@ class ComputeResource(resource.Resource): attr_type=attr.AttributeType.string_type), }) - actions = (start, stop, restart, suspend) + actions = (start, stop, restart, suspend, save) kind = kind.Kind(helpers.build_scheme('infrastructure'), 'compute', 'compute resource', attributes, 'compute/', actions=actions, diff --git a/ooi/tests/fakes.py b/ooi/tests/fakes.py index 6b536a9..650c940 100644 --- a/ooi/tests/fakes.py +++ b/ooi/tests/fakes.py @@ -128,6 +128,8 @@ linked_vm_id = uuid.uuid4().hex allocated_ip = "192.168.253.23" +action_loc_id = uuid.uuid4().hex + floating_ips = { tenants["foo"]["id"]: [], tenants["bar"]["id"]: [], @@ -370,6 +372,10 @@ def fake_query_results(): 'suspend; ' 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; ' 'class="action"; title="suspend compute instance"') + cats.append( + 'save; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; ' + 'class="action"; title="save compute instance"') # OCCI Templates cats.append( @@ -581,7 +587,9 @@ class FakeApp(object): if actions: action_path = "%s/action" % obj_path - self.routes[action_path] = webob.Response(status=202) + r = webob.Response(status=202) + r.headers["Location"] = action_loc_id + self.routes[action_path] = r def _populate_attached_volumes(self, path, server_list, vol_list): for s in server_list: @@ -692,7 +700,7 @@ class FakeApp(object): elif req.path_info.endswith("action"): body = req.json_body.copy() action = body.popitem() - if action[0] in ["os-start", "os-stop", "reboot", + if action[0] in ["os-start", "os-stop", "reboot", "createImage", "addFloatingIp", "removeFloatingIp", "removeSecurityGroup", "addSecurityGroup"]: diff --git a/ooi/tests/functional/middleware/test_compute_controller.py b/ooi/tests/functional/middleware/test_compute_controller.py index 8b72e40..3668000 100644 --- a/ooi/tests/functional/middleware/test_compute_controller.py +++ b/ooi/tests/functional/middleware/test_compute_controller.py @@ -82,6 +82,10 @@ def build_occi_server(server): 'rel="http://schemas.ogf.org/occi/' 'infrastructure/compute/action#suspend"' % (fakes.application_url, server_id)) + links.append('<%s/compute/%s?action=save>; ' + 'rel="http://schemas.ogf.org/occi/' + 'infrastructure/compute/action#save"' % + (fakes.application_url, server_id)) result = [] for c in cats: @@ -175,6 +179,32 @@ class TestComputeController(test_middleware.TestMiddleware): self.assertDefaults(resp) self.assertEqual(204, resp.status_code) + def test_save_action_vm(self): + tenant = fakes.tenants["foo"] + app = self.get_app() + + headers = { + 'Category': ( + 'save ;' + 'scheme="http://schemas.ogf.org/occi/infrastructure/' + 'compute/action#";' + 'class="action"'), + # There is no easy way to test that this was taken into account, + # but at least it should not fail if the attribute is there + 'X-OCCI-Attribute': 'name="foobarimg"', + } + for server in fakes.servers[tenant["id"]]: + req = self._build_req("/compute/%s?action=save" % server["id"], + tenant["id"], method="POST", + headers=headers) + resp = req.get_response(app) + self.assertDefaults(resp) + expected = [("X-OCCI-Location", + utils.join_url(self.application_url + "/", + "os_tpl/%s" % fakes.action_loc_id))] + self.assertEqual(200, resp.status_code) + self.assertExpectedResult(expected, resp) + def test_invalid_action(self): tenant = fakes.tenants["foo"] app = self.get_app() diff --git a/ooi/tests/unit/controllers/test_compute.py b/ooi/tests/unit/controllers/test_compute.py index ff7b413..2499b36 100644 --- a/ooi/tests/unit/controllers/test_compute.py +++ b/ooi/tests/unit/controllers/test_compute.py @@ -174,6 +174,54 @@ class TestComputeController(base.TestController): self.assertEqual([], ret) m_run_action.assert_called_with(mock.ANY, action, server_uuid) + @mock.patch.object(helpers.OpenStackHelper, "get_server") + @mock.patch("ooi.occi.validator.Validator") + @mock.patch.object(compute.Controller, "_save_server") + def test_run_action_save(self, m_save, m_validator, m_get_server): + tenant = fakes.tenants["foo"] + req = self._build_req(tenant["id"], path="/foo?action=save") + req.get_parser = mock.MagicMock() + server_uuid = uuid.uuid4().hex + server = {"status": "ACTIVE"} + m_get_server.return_value = server + ret = self.controller.run_action(req, server_uuid, None) + self.assertEqual(m_save.return_value, ret) + m_save.assert_called_with(mock.ANY, server_uuid, server, + mock.ANY) + + @mock.patch.object(helpers.OpenStackHelper, "run_action") + def test_save_server_no_name(self, m_run_action): + tenant = fakes.tenants["foo"] + req = self._build_req(tenant["id"], path="/foo?action=start") + server = {"name": "foo"} + server_uuid = uuid.uuid4().hex + m_run_action.return_value.headers = {"Location": "foobar"} + ret = self.controller._save_server(req, server_uuid, server, {}) + m_run_action.assert_called_with(mock.ANY, "save", server_uuid, + {"name": "foo"}) + self.assertIsInstance(ret, collection.Collection) + tpl = ret.mixins.pop() + self.assertIsInstance(tpl, templates.OpenStackOSTemplate) + self.assertEqual("foobar", tpl.term) + self.assertEqual("foo", tpl.title) + + @mock.patch.object(helpers.OpenStackHelper, "run_action") + def test_save_server_with_name(self, m_run_action): + tenant = fakes.tenants["foo"] + req = self._build_req(tenant["id"], path="/foo?action=start") + server = {"name": "foo"} + server_uuid = uuid.uuid4().hex + obj = {"attributes": {"name": "bar"}} + m_run_action.return_value.headers = {"Location": "foobar"} + ret = self.controller._save_server(req, server_uuid, server, obj) + m_run_action.assert_called_with(mock.ANY, "save", server_uuid, + {"name": "bar"}) + self.assertIsInstance(ret, collection.Collection) + tpl = ret.mixins.pop() + self.assertIsInstance(tpl, templates.OpenStackOSTemplate) + self.assertEqual("foobar", tpl.term) + self.assertEqual("bar", tpl.title) + @mock.patch.object(helpers.OpenStackHelper, "get_server_volumes_link") @mock.patch.object(helpers.OpenStackHelper, "get_image") @mock.patch.object(helpers.OpenStackHelper, "get_flavor") diff --git a/ooi/tests/unit/controllers/test_helpers.py b/ooi/tests/unit/controllers/test_helpers.py index 18bcc14..36f742f 100644 --- a/ooi/tests/unit/controllers/test_helpers.py +++ b/ooi/tests/unit/controllers/test_helpers.py @@ -455,8 +455,8 @@ class TestOpenStackHelper(TestBaseHelper): server_uuid = uuid.uuid4().hex action = "start" ret = self.helper.run_action(None, action, server_uuid) - self.assertIsNone(ret) - m.assert_called_with(None, action, server_uuid) + self.assertEqual(resp, ret) + m.assert_called_with(None, action, server_uuid, None) @mock.patch("ooi.api.helpers.exception_from_response") @mock.patch.object(helpers.OpenStackHelper, "_get_run_action_req") @@ -474,7 +474,7 @@ class TestOpenStackHelper(TestBaseHelper): None, action, server_uuid) - m.assert_called_with(None, action, server_uuid) + m.assert_called_with(None, action, server_uuid, None) m_exc.assert_called_with(resp) @mock.patch.object(helpers.OpenStackHelper, "_get_server_req") @@ -890,6 +890,7 @@ class TestOpenStackHelperReqs(TestBaseHelper): "resume": {"resume": None}, "unpause": {"unpause": None}, "restart": {"reboot": {"type": "SOFT"}}, + "save": {"createImage": None}, } path = "/%s/servers/%s/action" % (tenant["id"], server_uuid) @@ -898,6 +899,29 @@ class TestOpenStackHelperReqs(TestBaseHelper): os_req = self.helper._get_run_action_req(req, act, server_uuid) self.assertExpectedReq("POST", path, body, os_req) + def test_os_action_req_with_args(self): + tenant = fakes.tenants["foo"] + req = self._build_req(tenant["id"]) + server_uuid = uuid.uuid4().hex + + actions_map = { + "stop": "os-stop", + "start": "os-start", + "suspend": "suspend", + "resume": "resume", + "unpause": "unpause", + "restart": "reboot", + "save": "createImage", + } + + path = "/%s/servers/%s/action" % (tenant["id"], server_uuid) + + action_args = {"foo": "bar"} + for act, os_act in six.iteritems(actions_map): + os_req = self.helper._get_run_action_req(req, act, server_uuid, + action_args) + self.assertExpectedReq("POST", path, {os_act: action_args}, os_req) + def test_get_os_server_req(self): tenant = fakes.tenants["foo"] server_uuid = uuid.uuid4().hex diff --git a/ooi/tests/unit/controllers/test_query.py b/ooi/tests/unit/controllers/test_query.py index fcbeb65..c8b7504 100644 --- a/ooi/tests/unit/controllers/test_query.py +++ b/ooi/tests/unit/controllers/test_query.py @@ -86,6 +86,7 @@ class TestQueryController(base.TestController): compute.stop, compute.restart, compute.suspend, + compute.save, storage.online, storage.offline, @@ -155,6 +156,7 @@ class TestQueryController(base.TestController): compute.stop, compute.restart, compute.suspend, + compute.save, storage.online, storage.offline,