Merge "Include OCCI1.2 save action"
This commit is contained in:
commit
03ca1e68f1
@ -12,8 +12,10 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import os.path
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
import ooi.api.base
|
import ooi.api.base
|
||||||
@ -69,6 +71,21 @@ class Controller(ooi.api.base.Controller):
|
|||||||
|
|
||||||
return collection.Collection(resources=occi_compute_resources)
|
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):
|
def run_action(self, req, id, body):
|
||||||
action = req.GET.get("action", None)
|
action = req.GET.get("action", None)
|
||||||
occi_actions = [a.term for a in compute.ComputeResource.actions]
|
occi_actions = [a.term for a in compute.ComputeResource.actions]
|
||||||
@ -93,12 +110,17 @@ class Controller(ooi.api.base.Controller):
|
|||||||
scheme = {"category": compute.restart}
|
scheme = {"category": compute.restart}
|
||||||
elif action == "suspend":
|
elif action == "suspend":
|
||||||
scheme = {"category": compute.suspend}
|
scheme = {"category": compute.suspend}
|
||||||
|
elif action == "save":
|
||||||
|
scheme = {"category": compute.save}
|
||||||
else:
|
else:
|
||||||
raise exception.NotImplemented
|
raise exception.NotImplemented
|
||||||
|
|
||||||
validator = occi_validator.Validator(obj)
|
validator = occi_validator.Validator(obj)
|
||||||
validator.validate(scheme)
|
validator.validate(scheme)
|
||||||
|
|
||||||
|
if action == "save":
|
||||||
|
return self._save_server(req, id, server, obj)
|
||||||
|
else:
|
||||||
self.os_helper.run_action(req, action, id)
|
self.os_helper.run_action(req, action, id)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -295,7 +295,7 @@ class OpenStackHelper(BaseHelper):
|
|||||||
if response.status_int not in [204]:
|
if response.status_int not in [204]:
|
||||||
raise exception_from_response(response)
|
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)
|
tenant_id = self.tenant_from_req(req)
|
||||||
path = "/%s/servers/%s/action" % (tenant_id, server_id)
|
path = "/%s/servers/%s/action" % (tenant_id, server_id)
|
||||||
|
|
||||||
@ -306,23 +306,29 @@ class OpenStackHelper(BaseHelper):
|
|||||||
"resume": {"resume": None},
|
"resume": {"resume": None},
|
||||||
"unpause": {"unpause": None},
|
"unpause": {"unpause": None},
|
||||||
"restart": {"reboot": {"type": "SOFT"}},
|
"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)
|
body = json.dumps(action)
|
||||||
return self._get_req(req, path=path, body=body, method="POST")
|
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.
|
"""Run an action on a server.
|
||||||
|
|
||||||
:param req: the incoming request
|
:param req: the incoming request
|
||||||
:param action: the action to run
|
:param action: the action to run
|
||||||
:param server_id: server id to delete
|
: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)
|
response = os_req.get_response(self.app)
|
||||||
if response.status_int != 202:
|
if response.status_int != 202:
|
||||||
raise exception_from_response(response)
|
raise exception_from_response(response)
|
||||||
|
return response
|
||||||
|
|
||||||
def _get_server_req(self, req, server_id):
|
def _get_server_req(self, req, server_id):
|
||||||
tenant_id = self.tenant_from_req(req)
|
tenant_id = self.tenant_from_req(req)
|
||||||
|
@ -30,6 +30,9 @@ restart = action.Action(helpers.build_scheme('infrastructure/compute/action'),
|
|||||||
suspend = action.Action(helpers.build_scheme('infrastructure/compute/action'),
|
suspend = action.Action(helpers.build_scheme('infrastructure/compute/action'),
|
||||||
"suspend", "suspend compute instance")
|
"suspend", "suspend compute instance")
|
||||||
|
|
||||||
|
save = action.Action(helpers.build_scheme('infrastructure/compute/action'),
|
||||||
|
"save", "save compute instance")
|
||||||
|
|
||||||
|
|
||||||
class ComputeResource(resource.Resource):
|
class ComputeResource(resource.Resource):
|
||||||
attributes = attr.AttributeCollection({
|
attributes = attr.AttributeCollection({
|
||||||
@ -63,7 +66,7 @@ class ComputeResource(resource.Resource):
|
|||||||
attr_type=attr.AttributeType.string_type),
|
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',
|
kind = kind.Kind(helpers.build_scheme('infrastructure'), 'compute',
|
||||||
'compute resource', attributes, 'compute/',
|
'compute resource', attributes, 'compute/',
|
||||||
actions=actions,
|
actions=actions,
|
||||||
|
@ -132,6 +132,8 @@ allocated_ip = {"ip": "192.168.253.23",
|
|||||||
"pool": uuid.uuid4().hex,
|
"pool": uuid.uuid4().hex,
|
||||||
"instance_id": None}
|
"instance_id": None}
|
||||||
|
|
||||||
|
action_loc_id = uuid.uuid4().hex
|
||||||
|
|
||||||
floating_ips = {
|
floating_ips = {
|
||||||
tenants["foo"]["id"]: [],
|
tenants["foo"]["id"]: [],
|
||||||
tenants["bar"]["id"]: [],
|
tenants["bar"]["id"]: [],
|
||||||
@ -410,6 +412,10 @@ def fake_query_results():
|
|||||||
'suspend; '
|
'suspend; '
|
||||||
'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; '
|
'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; '
|
||||||
'class="action"; title="suspend compute instance"')
|
'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
|
# OCCI Templates
|
||||||
cats.append(
|
cats.append(
|
||||||
@ -627,7 +633,9 @@ class FakeApp(object):
|
|||||||
|
|
||||||
if actions:
|
if actions:
|
||||||
action_path = "%s/action" % obj_path
|
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):
|
def _populate_attached_volumes(self, path, server_list, vol_list):
|
||||||
for s in server_list:
|
for s in server_list:
|
||||||
@ -751,7 +759,7 @@ class FakeApp(object):
|
|||||||
elif req.path_info.endswith("action"):
|
elif req.path_info.endswith("action"):
|
||||||
body = req.json_body.copy()
|
body = req.json_body.copy()
|
||||||
action = body.popitem()
|
action = body.popitem()
|
||||||
if action[0] in ["os-start", "os-stop", "reboot",
|
if action[0] in ["os-start", "os-stop", "reboot", "createImage",
|
||||||
"addFloatingIp", "removeFloatingIp",
|
"addFloatingIp", "removeFloatingIp",
|
||||||
"removeSecurityGroup",
|
"removeSecurityGroup",
|
||||||
"addSecurityGroup"]:
|
"addSecurityGroup"]:
|
||||||
|
@ -82,6 +82,10 @@ def build_occi_server(server):
|
|||||||
'rel="http://schemas.ogf.org/occi/'
|
'rel="http://schemas.ogf.org/occi/'
|
||||||
'infrastructure/compute/action#suspend"' %
|
'infrastructure/compute/action#suspend"' %
|
||||||
(fakes.application_url, server_id))
|
(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 = []
|
result = []
|
||||||
for c in cats:
|
for c in cats:
|
||||||
@ -175,6 +179,32 @@ class TestComputeController(test_middleware.TestMiddleware):
|
|||||||
self.assertDefaults(resp)
|
self.assertDefaults(resp)
|
||||||
self.assertEqual(204, resp.status_code)
|
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):
|
def test_invalid_action(self):
|
||||||
tenant = fakes.tenants["foo"]
|
tenant = fakes.tenants["foo"]
|
||||||
app = self.get_app()
|
app = self.get_app()
|
||||||
|
@ -171,6 +171,54 @@ class TestComputeController(base.TestController):
|
|||||||
self.assertEqual([], ret)
|
self.assertEqual([], ret)
|
||||||
m_run_action.assert_called_with(mock.ANY, action, server_uuid)
|
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_server_volumes_link")
|
||||||
@mock.patch.object(helpers.OpenStackHelper, "get_image")
|
@mock.patch.object(helpers.OpenStackHelper, "get_image")
|
||||||
@mock.patch.object(helpers.OpenStackHelper, "get_flavor")
|
@mock.patch.object(helpers.OpenStackHelper, "get_flavor")
|
||||||
|
@ -455,8 +455,8 @@ class TestOpenStackHelper(TestBaseHelper):
|
|||||||
server_uuid = uuid.uuid4().hex
|
server_uuid = uuid.uuid4().hex
|
||||||
action = "start"
|
action = "start"
|
||||||
ret = self.helper.run_action(None, action, server_uuid)
|
ret = self.helper.run_action(None, action, server_uuid)
|
||||||
self.assertIsNone(ret)
|
self.assertEqual(resp, ret)
|
||||||
m.assert_called_with(None, action, server_uuid)
|
m.assert_called_with(None, action, server_uuid, None)
|
||||||
|
|
||||||
@mock.patch("ooi.api.helpers.exception_from_response")
|
@mock.patch("ooi.api.helpers.exception_from_response")
|
||||||
@mock.patch.object(helpers.OpenStackHelper, "_get_run_action_req")
|
@mock.patch.object(helpers.OpenStackHelper, "_get_run_action_req")
|
||||||
@ -474,7 +474,7 @@ class TestOpenStackHelper(TestBaseHelper):
|
|||||||
None,
|
None,
|
||||||
action,
|
action,
|
||||||
server_uuid)
|
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)
|
m_exc.assert_called_with(resp)
|
||||||
|
|
||||||
@mock.patch.object(helpers.OpenStackHelper, "_get_server_req")
|
@mock.patch.object(helpers.OpenStackHelper, "_get_server_req")
|
||||||
@ -890,6 +890,7 @@ class TestOpenStackHelperReqs(TestBaseHelper):
|
|||||||
"resume": {"resume": None},
|
"resume": {"resume": None},
|
||||||
"unpause": {"unpause": None},
|
"unpause": {"unpause": None},
|
||||||
"restart": {"reboot": {"type": "SOFT"}},
|
"restart": {"reboot": {"type": "SOFT"}},
|
||||||
|
"save": {"createImage": None},
|
||||||
}
|
}
|
||||||
|
|
||||||
path = "/%s/servers/%s/action" % (tenant["id"], server_uuid)
|
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)
|
os_req = self.helper._get_run_action_req(req, act, server_uuid)
|
||||||
self.assertExpectedReq("POST", path, body, os_req)
|
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):
|
def test_get_os_server_req(self):
|
||||||
tenant = fakes.tenants["foo"]
|
tenant = fakes.tenants["foo"]
|
||||||
server_uuid = uuid.uuid4().hex
|
server_uuid = uuid.uuid4().hex
|
||||||
|
@ -88,6 +88,7 @@ class TestQueryController(base.TestController):
|
|||||||
compute.stop,
|
compute.stop,
|
||||||
compute.restart,
|
compute.restart,
|
||||||
compute.suspend,
|
compute.suspend,
|
||||||
|
compute.save,
|
||||||
|
|
||||||
storage.online,
|
storage.online,
|
||||||
storage.offline,
|
storage.offline,
|
||||||
@ -158,6 +159,7 @@ class TestQueryController(base.TestController):
|
|||||||
compute.stop,
|
compute.stop,
|
||||||
compute.restart,
|
compute.restart,
|
||||||
compute.suspend,
|
compute.suspend,
|
||||||
|
compute.save,
|
||||||
|
|
||||||
storage.online,
|
storage.online,
|
||||||
storage.offline,
|
storage.offline,
|
||||||
|
Loading…
Reference in New Issue
Block a user