Include OCCI1.2 save action

OCCI1.2 creates new actions on compute resources, "save" creates
an snapshot of an existing image and includes that snapshot as
a valid OS Template.

Implements: blueprint snapshot-support
Change-Id: Ic416d3c5fd9757af4f949158a0a15c83775ea801
This commit is contained in:
Enol Fernandez 2017-01-17 10:23:30 +01:00 committed by Alvaro Lopez Garcia
parent 98af646b0c
commit f6fdeaf0b7
8 changed files with 155 additions and 12 deletions

View File

@ -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 = []

View File

@ -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)

View File

@ -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,

View File

@ -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"]:

View File

@ -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()

View File

@ -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")

View File

@ -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

View File

@ -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,