From f8bedd77499d3ddef31e1b50df6b43f9a5d3e999 Mon Sep 17 00:00:00 2001
From: Armando Migliaccio <armando.migliaccio@eu.citrix.com>
Date: Tue, 17 Jan 2012 18:45:53 +0000
Subject: [PATCH] blueprint host-aggregates: OSAPI extensions

This commit introduces the OSAPI admin extensions for host aggregates.
This is part of a series of commits that have started with change:

https://review.openstack.org/#change,3035

Change-Id: If0c92dca7d6f5d794f84bdb2adc172c55dec6e74
---
 .../openstack/compute/contrib/aggregates.py   | 223 +++++++++++
 .../compute/contrib/test_aggregates.py        | 372 ++++++++++++++++++
 2 files changed, 595 insertions(+)
 create mode 100644 nova/api/openstack/compute/contrib/aggregates.py
 create mode 100644 nova/tests/api/openstack/compute/contrib/test_aggregates.py

diff --git a/nova/api/openstack/compute/contrib/aggregates.py b/nova/api/openstack/compute/contrib/aggregates.py
new file mode 100644
index 000000000..c0b0001e6
--- /dev/null
+++ b/nova/api/openstack/compute/contrib/aggregates.py
@@ -0,0 +1,223 @@
+# Copyright (c) 2012 Citrix Systems, Inc.
+# All Rights Reserved.
+#
+#    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.
+
+"""The Aggregate admin API extension."""
+
+from webob import exc
+
+from nova.api.openstack import extensions
+from nova import compute
+from nova import exception
+from nova import log as logging
+
+
+LOG = logging.getLogger("nova.api.openstack.compute.contrib.host_aggregates")
+authorize = extensions.extension_authorizer('compute', 'aggregates')
+
+
+def _get_context(req):
+    return req.environ['nova.context']
+
+
+def get_host_from_body(fn):
+    """Makes sure that the host exists."""
+    def wrapped(self, req, id, body, *args, **kwargs):
+        if len(body) == 1 and "host" in body:
+            host = body['host']
+        else:
+            raise exc.HTTPBadRequest
+        return fn(self, req, id, host, *args, **kwargs)
+    return wrapped
+
+
+class AggregateController(object):
+    """The Host Aggregates API controller for the OpenStack API."""
+    def __init__(self):
+        self.api = compute.AggregateAPI()
+
+    def index(self, req):
+        """Returns a list a host aggregate's id, name, availability_zone."""
+        context = _get_context(req)
+        authorize(context)
+        aggregates = self.api.get_aggregate_list(context)
+        return {'aggregates': aggregates}
+
+    def create(self, req, body):
+        """Creates an aggregate, given its name and availablity_zone."""
+        context = _get_context(req)
+        authorize(context)
+
+        if len(body) != 1:
+            raise exc.HTTPBadRequest
+        try:
+            host_aggregate = body["aggregate"]
+            aggregate_name = host_aggregate["name"]
+            availability_zone = host_aggregate["availability_zone"]
+        except KeyError:
+            raise exc.HTTPBadRequest
+        if len(host_aggregate) != 2:
+            raise exc.HTTPBadRequest
+
+        try:
+            aggregate = self.api.create_aggregate(context, aggregate_name,
+                                                  availability_zone)
+        except exception.AggregateNameExists:
+            raise exc.HTTPConflict
+        return self._marshall_aggregate(aggregate)
+
+    def show(self, req, id):
+        """Shows the details of an aggregate, hosts and metadata included."""
+        context = _get_context(req)
+        authorize(context)
+        try:
+            aggregate = self.api.get_aggregate(context, id)
+        except exception.AggregateNotFound:
+            raise exc.HTTPNotFound
+        return self._marshall_aggregate(aggregate)
+
+    def update(self, req, id, body):
+        """Updates the name and/or availbility_zone of given aggregate."""
+        context = _get_context(req)
+        authorize(context)
+        aggregate = id
+
+        if len(body) != 1:
+            raise exc.HTTPBadRequest
+        try:
+            updates = body["aggregate"]
+        except KeyError:
+            raise exc.HTTPBadRequest
+
+        if len(updates) < 1:
+            raise exc.HTTPBadRequest
+
+        for key in updates.keys():
+            if not key in ["name", "availability_zone"]:
+                raise exc.HTTPBadRequest
+
+        try:
+            aggregate = self.api.update_aggregate(context, aggregate, updates)
+        except exception.AggregateNotFound:
+            raise exc.HTTPNotFound
+
+        return self._marshall_aggregate(aggregate)
+
+    def delete(self, req, id):
+        """Removes an aggregate by id."""
+        context = _get_context(req)
+        authorize(context)
+        aggregate_id = id
+        try:
+            self.api.delete_aggregate(context, aggregate_id)
+        except exception.AggregateNotFound:
+            raise exc.HTTPNotFound
+
+    def action(self, req, id, body):
+        _actions = {
+            'add_host': self._add_host,
+            'remove_host': self._remove_host,
+            'set_metadata': self._set_metadata,
+        }
+        for action, data in body.iteritems():
+            try:
+                return _actions[action](req, id, data)
+            except KeyError:
+                msg = _("Aggregates does not have %s action") % action
+                raise exc.HTTPBadRequest(explanation=msg)
+
+        raise exc.HTTPBadRequest(explanation=_("Invalid request body"))
+
+    @get_host_from_body
+    def _add_host(self, req, id, host):
+        """Adds a host to the specified aggregate."""
+        context = _get_context(req)
+        authorize(context)
+        aggregate = id
+        try:
+            aggregate = self.api.add_host_to_aggregate(context,
+                                                       aggregate, host)
+        except exception.AggregateNotFound:
+            raise exc.HTTPNotFound
+        except exception.ComputeHostNotFound:
+            raise exc.HTTPNotFound
+        except exception.AggregateHostConflict:
+            raise exc.HTTPConflict
+        except exception.AggregateHostExists:
+            raise exc.HTTPConflict
+        except exception.InvalidAggregateAction:
+            raise exc.HTTPConflict
+        return self._marshall_aggregate(aggregate)
+
+    @get_host_from_body
+    def _remove_host(self, req, id, host):
+        """Removes a host from the specified aggregate."""
+        context = _get_context(req)
+        authorize(context)
+        aggregate = id
+        try:
+            aggregate = self.api.remove_host_from_aggregate(context,
+                                                            aggregate, host)
+        except exception.AggregateNotFound:
+            raise exc.HTTPNotFound
+        except exception.AggregateHostNotFound:
+            raise exc.HTTPNotFound
+        except exception.InvalidAggregateAction:
+            raise exc.HTTPConflict
+        return self._marshall_aggregate(aggregate)
+
+    def _set_metadata(self, req, id, body):
+        """Replaces the aggregate's existing metadata with new metadata."""
+        context = _get_context(req)
+        authorize(context)
+        aggregate = id
+
+        if len(body) != 1:
+            raise exc.HTTPBadRequest
+
+        try:
+            metadata = body["metadata"]
+        except KeyError:
+            raise exc.HTTPBadRequest
+
+        try:
+            aggregate = self.api.update_aggregate_metadata(context,
+                                                           aggregate, metadata)
+        except exception.AggregateNotFound:
+            raise exc.HTTPNotFound
+
+        return self._marshall_aggregate(aggregate)
+
+    def _marshall_aggregate(self, aggregate):
+        return {"aggregate": aggregate}
+
+
+class Aggregates(extensions.ExtensionDescriptor):
+    """Admin-only aggregate administration"""
+
+    name = "Aggregates"
+    alias = "os-aggregates"
+    namespace = "http://docs.openstack.org/compute/ext/aggregates/api/v1.1"
+    updated = "2012-01-12T00:00:00+00:00"
+
+    def __init__(self, ext_mgr):
+        ext_mgr.register(self)
+
+    def get_resources(self):
+        resources = []
+        res = extensions.ResourceExtension('os-aggregates',
+                AggregateController(),
+                member_actions={"action": "POST", })
+        resources.append(res)
+        return resources
diff --git a/nova/tests/api/openstack/compute/contrib/test_aggregates.py b/nova/tests/api/openstack/compute/contrib/test_aggregates.py
new file mode 100644
index 000000000..a00dceba2
--- /dev/null
+++ b/nova/tests/api/openstack/compute/contrib/test_aggregates.py
@@ -0,0 +1,372 @@
+# Copyright (c) 2012 Citrix Systems, Inc.
+# All Rights Reserved.
+#
+#    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.
+
+"""Tests for the aggregates admin api."""
+
+from webob import exc
+
+from nova.api.openstack.compute.contrib import aggregates
+from nova import context
+from nova import exception
+from nova import log as logging
+from nova import test
+
+
+LOG = logging.getLogger('nova.tests.aggregates')
+AGGREGATE_LIST = [
+        {"name": "aggregate1", "id": "1", "availability_zone": "nova1"},
+        {"name": "aggregate2", "id": "2", "availability_zone": "nova1"},
+        {"name": "aggregate3", "id": "3", "availability_zone": "nova2"},
+        {"name": "aggregate1", "id": "4", "availability_zone": "nova1"}]
+AGGREGATE = {"name": "aggregate1",
+                  "id": "1",
+                  "availability_zone": "nova1",
+                  "metadata": {"foo": "bar"},
+                  "hosts": ["host1, host2"]}
+
+
+class FakeRequest(object):
+    environ = {"nova.context": context.get_admin_context()}
+
+
+class AggregateTestCase(test.TestCase):
+    """Test Case for aggregates admin api."""
+
+    def setUp(self):
+        super(AggregateTestCase, self).setUp()
+        self.controller = aggregates.AggregateController()
+        self.req = FakeRequest()
+        self.context = self.req.environ['nova.context']
+
+    def test_index(self):
+        def stub_list_aggregates(context):
+            if context == None:
+                raise Exception()
+            return AGGREGATE_LIST
+        self.stubs.Set(self.controller.api, 'get_aggregate_list',
+                       stub_list_aggregates)
+
+        result = self.controller.index(self.req)
+
+        self.assertEqual(AGGREGATE_LIST, result["aggregates"])
+
+    def test_create(self):
+        def stub_create_aggregate(context, name, availability_zone):
+            self.assertEqual(context, self.context, "context")
+            self.assertEqual("test", name, "name")
+            self.assertEqual("nova1", availability_zone, "availability_zone")
+            return AGGREGATE
+        self.stubs.Set(self.controller.api, "create_aggregate",
+                       stub_create_aggregate)
+
+        result = self.controller.create(self.req, {"aggregate":
+                                          {"name": "test",
+                                           "availability_zone": "nova1"}})
+        self.assertEqual(AGGREGATE, result["aggregate"])
+
+    def test_create_with_duplicate_aggregate_name(self):
+        def stub_create_aggregate(context, name, availability_zone):
+            raise exception.AggregateNameExists
+        self.stubs.Set(self.controller.api, "create_aggregate",
+                       stub_create_aggregate)
+
+        self.assertRaises(exc.HTTPConflict, self.controller.create,
+                          self.req, {"aggregate":
+                                     {"name": "test",
+                                      "availability_zone": "nova1"}})
+
+    def test_create_with_no_aggregate(self):
+        self.assertRaises(exc.HTTPBadRequest, self.controller.create,
+                          self.req, {"foo":
+                                     {"name": "test",
+                                      "availability_zone": "nova1"}})
+
+    def test_create_with_no_name(self):
+        self.assertRaises(exc.HTTPBadRequest, self.controller.create,
+                          self.req, {"aggregate":
+                                     {"foo": "test",
+                                      "availability_zone": "nova1"}})
+
+    def test_create_with_no_availability_zone(self):
+        self.assertRaises(exc.HTTPBadRequest, self.controller.create,
+                          self.req, {"aggregate":
+                                     {"name": "test",
+                                      "foo": "nova1"}})
+
+    def test_create_with_extra_invalid_arg(self):
+        self.assertRaises(exc.HTTPBadRequest, self.controller.create,
+                          self.req, dict(name="test",
+                                         availablity_zone="nova1",
+                                         foo='bar'))
+
+    def test_show(self):
+        def stub_get_aggregate(context, id):
+            self.assertEqual(context, self.context, "context")
+            self.assertEqual("1", id, "id")
+            return AGGREGATE
+        self.stubs.Set(self.controller.api, 'get_aggregate',
+                       stub_get_aggregate)
+
+        aggregate = self.controller.show(self.req, "1")
+
+        self.assertEqual(AGGREGATE, aggregate["aggregate"])
+
+    def test_show_with_invalid_id(self):
+        def stub_get_aggregate(context, id):
+            raise exception.AggregateNotFound(aggregate_id=2)
+
+        self.stubs.Set(self.controller.api, 'get_aggregate',
+                       stub_get_aggregate)
+
+        self.assertRaises(exc.HTTPNotFound,
+                          self.controller.show, self.req, "2")
+
+    def test_update(self):
+        body = {"aggregate": {"name": "new_name",
+                              "availability_zone": "nova1"}}
+
+        def stub_update_aggregate(context, aggregate, values):
+            self.assertEqual(context, self.context, "context")
+            self.assertEqual("1", aggregate, "aggregate")
+            self.assertEqual(body["aggregate"], values, "values")
+            return AGGREGATE
+        self.stubs.Set(self.controller.api, "update_aggregate",
+                       stub_update_aggregate)
+
+        result = self.controller.update(self.req, "1", body=body)
+
+        self.assertEqual(AGGREGATE, result["aggregate"])
+
+    def test_update_with_only_name(self):
+        body = {"aggregate": {"name": "new_name"}}
+
+        def stub_update_aggregate(context, aggregate, values):
+            return AGGREGATE
+        self.stubs.Set(self.controller.api, "update_aggregate",
+                       stub_update_aggregate)
+
+        result = self.controller.update(self.req, "1", body=body)
+
+        self.assertEqual(AGGREGATE, result["aggregate"])
+
+    def test_update_with_only_availability_zone(self):
+        body = {"aggregate": {"availability_zone": "nova1"}}
+
+        def stub_update_aggregate(context, aggregate, values):
+            return AGGREGATE
+        self.stubs.Set(self.controller.api, "update_aggregate",
+                       stub_update_aggregate)
+
+        result = self.controller.update(self.req, "1", body=body)
+
+        self.assertEqual(AGGREGATE, result["aggregate"])
+
+    def test_update_with_no_updates(self):
+        test_metadata = {"aggregate": {}}
+        self.assertRaises(exc.HTTPBadRequest, self.controller.update,
+                          self.req, "2", body=test_metadata)
+
+    def test_update_with_no_update_key(self):
+        test_metadata = {"asdf": {}}
+        self.assertRaises(exc.HTTPBadRequest, self.controller.update,
+                          self.req, "2", body=test_metadata)
+
+    def test_update_with_wrong_updates(self):
+        test_metadata = {"aggregate": {"status": "disable",
+                                     "foo": "bar"}}
+        self.assertRaises(exc.HTTPBadRequest, self.controller.update,
+                          self.req, "2", body=test_metadata)
+
+    def test_update_with_bad_host_aggregate(self):
+        test_metadata = {"aggregate": {"name": "test_name"}}
+
+        def stub_update_aggregate(context, aggregate, metadata):
+            raise exception.AggregateNotFound(aggregate_id=2)
+        self.stubs.Set(self.controller.api, "update_aggregate",
+                       stub_update_aggregate)
+
+        self.assertRaises(exc.HTTPNotFound, self.controller.update,
+                self.req, "2", body=test_metadata)
+
+    def test_add_host(self):
+        def stub_add_host_to_aggregate(context, aggregate, host):
+            self.assertEqual(context, self.context, "context")
+            self.assertEqual("1", aggregate, "aggregate")
+            self.assertEqual("host1", host, "host")
+            return AGGREGATE
+        self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+                       stub_add_host_to_aggregate)
+
+        aggregate = self.\
+                        controller.action(self.req, "1",
+                                          body={"add_host": {"host": "host1"}})
+
+        self.assertEqual(aggregate["aggregate"], AGGREGATE)
+
+    def test_add_host_with_already_added_to_another_aggregate(self):
+        def stub_add_host_to_aggregate(context, aggregate, host):
+            raise exception.AggregateHostConflict()
+        self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+                       stub_add_host_to_aggregate)
+
+        self.assertRaises(exc.HTTPConflict, self.controller.action,
+                          self.req, "duplicate_aggregate",
+                          body={"add_host": {"host": "host1"}})
+
+    def test_add_host_with_already_added_host(self):
+        def stub_add_host_to_aggregate(context, aggregate, host):
+            raise exception.AggregateHostExists()
+        self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+                       stub_add_host_to_aggregate)
+
+        self.assertRaises(exc.HTTPConflict, self.controller.action,
+                          self.req, "duplicate_aggregate",
+                          body={"add_host": {"host": "host1"}})
+
+    def test_add_host_with_bad_aggregate(self):
+        def stub_add_host_to_aggregate(context, aggregate, host):
+            raise exception.AggregateNotFound()
+        self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+                       stub_add_host_to_aggregate)
+
+        self.assertRaises(exc.HTTPNotFound, self.controller.action,
+                          self.req, "bogus_aggregate",
+                          body={"add_host": {"host": "host1"}})
+
+    def test_add_host_with_bad_host(self):
+        def stub_add_host_to_aggregate(context, aggregate, host):
+            raise exception.ComputeHostNotFound()
+        self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+                       stub_add_host_to_aggregate)
+
+        self.assertRaises(exc.HTTPNotFound, self.controller.action,
+                          self.req, "bogus_aggregate",
+                          body={"add_host": {"host": "host1"}})
+
+    def test_add_host_with_host_in_wrong_availability_zone(self):
+        def stub_add_host_to_aggregate(context, aggregate, host):
+            raise exception.InvalidAggregateAction()
+        self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+                       stub_add_host_to_aggregate)
+
+        self.assertRaises(exc.HTTPConflict, self.controller.action,
+                          self.req, "bogus_aggregate",
+                          body={"add_host": {"host": "host1"}})
+
+    def test_add_host_with_missing_host(self):
+        self.assertRaises(exc.HTTPBadRequest, self.controller.action,
+                self.req, "1", body={"asdf": "asdf"})
+
+    def test_remove_host(self):
+        def stub_remove_host_from_aggregate(context, aggregate, host):
+            self.assertEqual(context, self.context, "context")
+            self.assertEqual("1", aggregate, "aggregate")
+            self.assertEqual("host1", host, "host")
+            stub_remove_host_from_aggregate.called = True
+        self.stubs.Set(self.controller.api,
+                       "remove_host_from_aggregate",
+                       stub_remove_host_from_aggregate)
+        self.controller.action(self.req, "1",
+                               body={"remove_host": {"host": "host1"}})
+
+        self.assertTrue(stub_remove_host_from_aggregate.called)
+
+    def test_remove_host_with_bad_aggregate(self):
+        def stub_remove_host_from_aggregate(context, aggregate, host):
+            raise exception.AggregateNotFound()
+        self.stubs.Set(self.controller.api,
+                       "remove_host_from_aggregate",
+                       stub_remove_host_from_aggregate)
+
+        self.assertRaises(exc.HTTPNotFound, self.controller.action,
+                          self.req, "bogus_aggregate",
+                          body={"remove_host": {"host": "host1"}})
+
+    def test_remove_host_with_bad_host(self):
+        def stub_remove_host_from_aggregate(context, aggregate, host):
+            raise exception.AggregateHostNotFound()
+        self.stubs.Set(self.controller.api,
+                       "remove_host_from_aggregate",
+                       stub_remove_host_from_aggregate)
+
+        self.assertRaises(exc.HTTPNotFound, self.controller.action,
+                          self.req, "bogus_aggregate",
+                          body={"remove_host": {"host": "host1"}})
+
+    def test_remove_host_with_missing_host(self):
+        self.assertRaises(exc.HTTPBadRequest, self.controller.action,
+                self.req, "1", body={"asdf": "asdf"})
+
+    def test_remove_host_with_extra_param(self):
+        self.assertRaises(exc.HTTPBadRequest, self.controller.action,
+                self.req, "1", body={"asdf": "asdf", "host": "asdf"})
+
+    def test_set_metadata(self):
+        body = {"set_metadata": {"metadata": {"foo": "bar"}}}
+
+        def stub_update_aggregate(context, aggregate, values):
+            self.assertEqual(context, self.context, "context")
+            self.assertEqual("1", aggregate, "aggregate")
+            self.assertDictMatch(body["set_metadata"]['metadata'], values)
+            return AGGREGATE
+        self.stubs.Set(self.controller.api,
+                       "update_aggregate_metadata",
+                       stub_update_aggregate)
+
+        result = self.controller.action(self.req, "1", body=body)
+
+        self.assertEqual(AGGREGATE, result["aggregate"])
+
+    def test_set_metadata_with_bad_host_aggregate(self):
+        body = {"set_metadata": {"metadata": {"foo": "bar"}}}
+
+        def stub_update_aggregate(context, aggregate, metadata):
+            raise exception.AggregateNotFound()
+        self.stubs.Set(self.controller.api,
+                       "update_aggregate_metadata",
+                       stub_update_aggregate)
+
+        self.assertRaises(exc.HTTPNotFound, self.controller.action,
+                self.req, "bad_aggregate", body=body)
+
+    def test_set_metadata_with_missing_metadata(self):
+        body = {"asdf": {"foo": "bar"}}
+        self.assertRaises(exc.HTTPBadRequest, self.controller.action,
+                          self.req, "bad_aggregate", body=body)
+
+    def test_set_metadata_with_extra_params(self):
+        body = {"metadata": {"foo": "bar"}, "asdf": {"foo": "bar"}}
+        self.assertRaises(exc.HTTPBadRequest, self.controller.action,
+                          self.req, "bad_aggregate", body=body)
+
+    def test_delete_aggregate(self):
+        def stub_delete_aggregate(context, aggregate):
+            self.assertEqual(context, self.context, "context")
+            self.assertEqual("1", aggregate, "aggregate")
+            stub_delete_aggregate.called = True
+        self.stubs.Set(self.controller.api, "delete_aggregate",
+                       stub_delete_aggregate)
+
+        self.controller.delete(self.req, "1")
+        self.assertTrue(stub_delete_aggregate.called)
+
+    def test_delete_aggregate_with_bad_aggregate(self):
+        def stub_delete_aggregate(context, aggregate):
+            raise exception.AggregateNotFound()
+        self.stubs.Set(self.controller.api, "delete_aggregate",
+                       stub_delete_aggregate)
+
+        self.assertRaises(exc.HTTPNotFound, self.controller.delete,
+                self.req, "bogus_aggregate")