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
|
||||
# under the License.
|
||||
|
||||
import os.path
|
||||
import uuid
|
||||
|
||||
import six.moves.urllib.parse as urlparse
|
||||
import webob.exc
|
||||
|
||||
import ooi.api.base
|
||||
@ -69,6 +71,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]
|
||||
@ -93,14 +110,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 = []
|
||||
|
@ -295,7 +295,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)
|
||||
|
||||
@ -306,23 +306,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)
|
||||
|
@ -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,
|
||||
|
@ -132,6 +132,8 @@ allocated_ip = {"ip": "192.168.253.23",
|
||||
"pool": uuid.uuid4().hex,
|
||||
"instance_id": None}
|
||||
|
||||
action_loc_id = uuid.uuid4().hex
|
||||
|
||||
floating_ips = {
|
||||
tenants["foo"]["id"]: [],
|
||||
tenants["bar"]["id"]: [],
|
||||
@ -410,6 +412,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(
|
||||
@ -627,7 +633,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:
|
||||
@ -751,7 +759,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"]:
|
||||
|
@ -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()
|
||||
|
@ -171,6 +171,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")
|
||||
|
@ -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
|
||||
|
@ -88,6 +88,7 @@ class TestQueryController(base.TestController):
|
||||
compute.stop,
|
||||
compute.restart,
|
||||
compute.suspend,
|
||||
compute.save,
|
||||
|
||||
storage.online,
|
||||
storage.offline,
|
||||
@ -158,6 +159,7 @@ class TestQueryController(base.TestController):
|
||||
compute.stop,
|
||||
compute.restart,
|
||||
compute.suspend,
|
||||
compute.save,
|
||||
|
||||
storage.online,
|
||||
storage.offline,
|
||||
|
Loading…
Reference in New Issue
Block a user