diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index ace0dc9bb4ad..b4323b09756a 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -160,6 +160,14 @@ "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1", "updated": "2011-09-21T00:00:00+00:00" }, + { + "alias": "os-cell-capacities", + "description": "Adding functionality to get cell capacities.", + "links": [], + "name": "CellCapacities", + "namespace": "http://docs.openstack.org/compute/ext/cell_capacities/api/v1.1", + "updated": "2013-05-27T00:00:00+00:00" + }, { "alias": "os-certificates", "description": "Certificates support.", diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index 23d8b30d7d05..26361e7192e6 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -68,6 +68,9 @@ listing neighbor cells, and getting the capabilities of the local cell. + + Adds functionality to get cell capacities. + Certificates support. diff --git a/doc/api_samples/os-cell-capacities/cells-capacities-resp.json b/doc/api_samples/os-cell-capacities/cells-capacities-resp.json new file mode 100644 index 000000000000..b926f8d1df27 --- /dev/null +++ b/doc/api_samples/os-cell-capacities/cells-capacities-resp.json @@ -0,0 +1,18 @@ +{ + "cell": { + "capacities": { + "ram_free": { + "units_by_mb": { + "8192": 0, "512": 13, "4096": 1, "2048": 3, "16384": 0 + }, + "total_mb": 7680 + }, + "disk_free": { + "units_by_mb": { + "81920": 11, "20480": 46, "40960": 23, "163840": 5, "0": 0 + }, + "total_mb": 1052672 + } + } + } +} diff --git a/doc/api_samples/os-cell-capacities/cells-capacities-resp.xml b/doc/api_samples/os-cell-capacities/cells-capacities-resp.xml new file mode 100644 index 000000000000..63672b00bd56 --- /dev/null +++ b/doc/api_samples/os-cell-capacities/cells-capacities-resp.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/contrib/cell_capacities.py b/nova/api/openstack/compute/contrib/cell_capacities.py new file mode 100644 index 000000000000..ae8b42336058 --- /dev/null +++ b/nova/api/openstack/compute/contrib/cell_capacities.py @@ -0,0 +1,27 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Rackspace Hosting +# +# 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 + +from nova.api.openstack import extensions + + +class Cell_capacities(extensions.ExtensionDescriptor): + """Adding functionality to get cell capacities.""" + + name = "CellCapacities" + alias = "os-cell-capacities" + namespace = ("http://docs.openstack.org/compute/ext/" + "cell_capacities/api/v1.1") + updated = "2013-05-27T00:00:00+00:00" diff --git a/nova/api/openstack/compute/contrib/cells.py b/nova/api/openstack/compute/contrib/cells.py index 03597ff0e71c..4e809d70e696 100644 --- a/nova/api/openstack/compute/contrib/cells.py +++ b/nova/api/openstack/compute/contrib/cells.py @@ -52,8 +52,33 @@ def make_cell(elem): cap = xmlutil.SubTemplateElement(caps, xmlutil.Selector(0), selector=xmlutil.get_items) cap.text = 1 + make_capacity(elem) +def make_capacity(cell): + + def get_units_by_mb(capacity_info): + return capacity_info['units_by_mb'].items() + + capacity = xmlutil.SubTemplateElement(cell, 'capacities', + selector='capacities') + + ram_free = xmlutil.SubTemplateElement(capacity, 'ram_free', + selector='ram_free') + ram_free.set('total_mb', 'total_mb') + unit_by_mb = xmlutil.SubTemplateElement(ram_free, 'unit_by_mb', + selector=get_units_by_mb) + unit_by_mb.set('mb', 0) + unit_by_mb.set('unit', 1) + + disk_free = xmlutil.SubTemplateElement(capacity, 'disk_free', + selector='disk_free') + disk_free.set('total_mb', 'total_mb') + unit_by_mb = xmlutil.SubTemplateElement(disk_free, 'unit_by_mb', + selector=get_units_by_mb) + unit_by_mb.set('mb', 0) + unit_by_mb.set('unit', 1) + cell_nsmap = {None: wsgi.XMLNS_V10} @@ -123,9 +148,10 @@ def _scrub_cell(cell, detail=False): class Controller(object): """Controller for Cell resources.""" - def __init__(self): + def __init__(self, ext_mgr): self.compute_api = compute.API() self.cells_rpcapi = cells_rpcapi.CellsAPI() + self.ext_mgr = ext_mgr def _get_cells(self, ctxt, req, detail=False): """Return all cells.""" @@ -167,6 +193,25 @@ class Controller(object): 'capabilities': cell_capabs} return dict(cell=cell) + @wsgi.serializers(xml=CellTemplate) + def capacities(self, req, id=None): + """Return capacities for a given cell or all cells.""" + # TODO(kaushikc): return capacities as a part of cell info and + # cells detail calls in v3, along with capabilities + if not self.ext_mgr.is_loaded('os-cell-capacities'): + raise exc.HTTPNotFound() + + context = req.environ['nova.context'] + authorize(context) + try: + capacities = self.cells_rpcapi.get_capacities(context, + cell_name=id) + except exception.CellNotFound: + msg = (_("Cell %(id)s not found.") % {'id': id}) + raise exc.HTTPNotFound(explanation=msg) + + return dict(cell={"capacities": capacities}) + @wsgi.serializers(xml=CellTemplate) def show(self, req, id): """Return data about the given cell name. 'id' is a cell name.""" @@ -283,15 +328,20 @@ class Cells(extensions.ExtensionDescriptor): name = "Cells" alias = "os-cells" namespace = "http://docs.openstack.org/compute/ext/cells/api/v1.1" - updated = "2011-09-21T00:00:00+00:00" + updated = "2013-05-14T00:00:00+00:00" def get_resources(self): coll_actions = { 'detail': 'GET', 'info': 'GET', 'sync_instances': 'POST', - } + 'capacities': 'GET', + } + memb_actions = { + 'capacities': 'GET', + } res = extensions.ResourceExtension('os-cells', - Controller(), collection_actions=coll_actions) + Controller(self.ext_mgr), collection_actions=coll_actions, + member_actions=memb_actions) return [res] diff --git a/nova/cells/manager.py b/nova/cells/manager.py index ba909c0348d4..f776c542e4ae 100644 --- a/nova/cells/manager.py +++ b/nova/cells/manager.py @@ -64,7 +64,7 @@ class CellsManager(manager.Manager): Scheduling requests get passed to the scheduler class. """ - RPC_API_VERSION = '1.8' + RPC_API_VERSION = '1.9' def __init__(self, *args, **kwargs): # Mostly for tests. @@ -387,3 +387,6 @@ class CellsManager(manager.Manager): instance['cell_name'], instance_uuid, console_port, console_type) return response.value_or_raise() + + def get_capacities(self, ctxt, cell_name): + return self.state_manager.get_capacities(cell_name) diff --git a/nova/cells/rpcapi.py b/nova/cells/rpcapi.py index 4a45af255b77..dd757c818b9a 100644 --- a/nova/cells/rpcapi.py +++ b/nova/cells/rpcapi.py @@ -51,6 +51,7 @@ class CellsAPI(rpc_proxy.RpcProxy): 1.6 - Adds consoleauth_delete_tokens() and validate_console_port() 1.7 - Adds service_update() 1.8 - Adds build_instances(), deprecates schedule_run_instance() + 1.9 - Adds get_capacities() ''' BASE_RPC_API_VERSION = '1.0' @@ -292,3 +293,8 @@ class CellsAPI(rpc_proxy.RpcProxy): console_port=console_port, console_type=console_type), version='1.6') + + def get_capacities(self, ctxt, cell_name=None): + return self.call(ctxt, + self.make_msg('get_capacities', cell_name=cell_name), + version='1.9') diff --git a/nova/cells/state.py b/nova/cells/state.py index defa897a9219..37813d581b37 100644 --- a/nova/cells/state.py +++ b/nova/cells/state.py @@ -25,6 +25,7 @@ from oslo.config import cfg from nova.cells import rpc_driver from nova import context from nova.db import base +from nova import exception from nova.openstack.common import log as logging from nova.openstack.common import timeutils from nova import utils @@ -361,3 +362,11 @@ class CellStateManager(base.Base): for cell in self.child_cells.values(): self._add_to_dict(capacities, cell.capacities) return capacities + + @sync_from_db + def get_capacities(self, cell_name=None): + if not cell_name or cell_name == self.my_cell_state.name: + return self.get_our_capacities() + if cell_name in self.child_cells: + return self.child_cells[cell_name].capacities + raise exception.CellNotFound(cell_name=cell_name) diff --git a/nova/tests/api/openstack/compute/contrib/test_cells.py b/nova/tests/api/openstack/compute/contrib/test_cells.py index bf6bff27c912..a9e77693ebec 100644 --- a/nova/tests/api/openstack/compute/contrib/test_cells.py +++ b/nova/tests/api/openstack/compute/contrib/test_cells.py @@ -19,6 +19,7 @@ from lxml import etree from webob import exc from nova.api.openstack.compute.contrib import cells as cells_ext +from nova.api.openstack import extensions from nova.api.openstack import xmlutil from nova.cells import rpcapi as cells_rpcapi from nova import context @@ -87,7 +88,8 @@ class CellsTest(test.TestCase): self.stubs.Set(cells_rpcapi.CellsAPI, 'get_cell_info_for_neighbors', fake_cells_api_get_all_cell_info) - self.controller = cells_ext.Controller() + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = cells_ext.Controller(self.ext_mgr) self.context = context.get_admin_context() def _get_request(self, resource): @@ -281,6 +283,74 @@ class CellsTest(test.TestCase): self.assertEqual(cell_caps['cap1'], 'a;b') self.assertEqual(cell_caps['cap2'], 'c;d') + def test_show_capacities(self): + self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True) + self.mox.StubOutWithMock(self.controller.cells_rpcapi, + 'get_capacities') + response = {"ram_free": + {"units_by_mb": {"8192": 0, "512": 13, + "4096": 1, "2048": 3, "16384": 0}, + "total_mb": 7680}, + "disk_free": + {"units_by_mb": {"81920": 11, "20480": 46, + "40960": 23, "163840": 5, "0": 0}, + "total_mb": 1052672} + } + self.controller.cells_rpcapi.\ + get_capacities(self.context, cell_name=None).AndReturn(response) + self.mox.ReplayAll() + req = self._get_request("cells/capacities") + req.environ["nova.context"] = self.context + res_dict = self.controller.capacities(req) + self.assertEqual(response, res_dict['cell']['capacities']) + + def test_show_capacity_fails_with_non_admin_context(self): + self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True) + rules = {"compute_extension:cells": "is_admin:true"} + self.policy.set_rules(rules) + + self.mox.ReplayAll() + req = self._get_request("cells/capacities") + req.environ["nova.context"] = self.context + req.environ["nova.context"].is_admin = False + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.capacities, req) + + def test_show_capacities_for_invalid_cell(self): + self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True) + self.mox.StubOutWithMock(self.controller.cells_rpcapi, + 'get_capacities') + self.controller.cells_rpcapi. \ + get_capacities(self.context, cell_name="invalid_cell").AndRaise( + exception.CellNotFound(cell_name="invalid_cell")) + self.mox.ReplayAll() + req = self._get_request("cells/invalid_cell/capacities") + req.environ["nova.context"] = self.context + self.assertRaises(exc.HTTPNotFound, + self.controller.capacities, req, "invalid_cell") + + def test_show_capacities_for_cell(self): + self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True) + self.mox.StubOutWithMock(self.controller.cells_rpcapi, + 'get_capacities') + response = {"ram_free": + {"units_by_mb": {"8192": 0, "512": 13, + "4096": 1, "2048": 3, "16384": 0}, + "total_mb": 7680}, + "disk_free": + {"units_by_mb": {"81920": 11, "20480": 46, + "40960": 23, "163840": 5, "0": 0}, + "total_mb": 1052672} + } + self.controller.cells_rpcapi.\ + get_capacities(self.context, cell_name='cell_name').\ + AndReturn(response) + self.mox.ReplayAll() + req = self._get_request("cells/capacities") + req.environ["nova.context"] = self.context + res_dict = self.controller.capacities(req, 'cell_name') + self.assertEqual(response, res_dict['cell']['capacities']) + def test_sync_instances(self): call_info = {} diff --git a/nova/tests/cells/test_cells_manager.py b/nova/tests/cells/test_cells_manager.py index 543ff66e7af6..4e35cd81802c 100644 --- a/nova/tests/cells/test_cells_manager.py +++ b/nova/tests/cells/test_cells_manager.py @@ -53,6 +53,7 @@ class CellsManagerClassTestCase(test.TestCase): self.our_cell = 'grandchild-cell1' self.cells_manager = fakes.get_cells_manager(self.our_cell) self.msg_runner = self.cells_manager.msg_runner + self.state_manager = fakes.get_state_manager(self.our_cell) self.driver = self.cells_manager.driver self.ctxt = 'fake_context' @@ -513,6 +514,17 @@ class CellsManagerClassTestCase(test.TestCase): self.cells_manager.consoleauth_delete_tokens(self.ctxt, instance_uuid=instance_uuid) + def test_get_capacities(self): + cell_name = 'cell_name' + response = {"ram_free": + {"units_by_mb": {"64": 20, "128": 10}, "total_mb": 1491}} + self.mox.StubOutWithMock(self.state_manager, + 'get_capacities') + self.state_manager.get_capacities(cell_name).AndReturn(response) + self.mox.ReplayAll() + self.assertEqual(response, + self.cells_manager.get_capacities(self.ctxt, cell_name)) + def test_validate_console_port(self): instance_uuid = 'fake-instance-uuid' cell_name = 'fake-cell-name' diff --git a/nova/tests/cells/test_cells_rpcapi.py b/nova/tests/cells/test_cells_rpcapi.py index 172b54831378..e44c0be4a049 100644 --- a/nova/tests/cells/test_cells_rpcapi.py +++ b/nova/tests/cells/test_cells_rpcapi.py @@ -129,6 +129,16 @@ class CellsAPITestCase(test.TestCase): self._check_result(call_info, 'build_instances', expected_args, version=1.8) + def test_get_capacities(self): + capacity_info = {"capacity": "info"} + call_info = self._stub_rpc_method('call', + result=capacity_info) + result = self.cells_rpcapi.get_capacities(self.fake_context, + cell_name="name") + self._check_result(call_info, 'get_capacities', + {'cell_name': 'name'}, version='1.9') + self.assertEqual(capacity_info, result) + def test_instance_update_at_top(self): fake_info_cache = {'id': 1, 'instance': 'fake_instance', diff --git a/nova/tests/cells/test_cells_state_manager.py b/nova/tests/cells/test_cells_state_manager.py index 078578926c1f..03dc37ed409c 100644 --- a/nova/tests/cells/test_cells_state_manager.py +++ b/nova/tests/cells/test_cells_state_manager.py @@ -13,11 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. """ -Tests For CellsStateManager +Tests For CellStateManager """ from nova.cells import state from nova import db +from nova.db.sqlalchemy import models +from nova import exception from nova import test @@ -120,9 +122,47 @@ class TestCellsStateManager(test.TestCase): units = 2 # 2 on host 3 self.assertEqual(units, cap['disk_free']['units_by_mb'][str(sz)]) - def _capacity(self, reserve_percent): + def _get_state_manager(self, reserve_percent=0.0): self.flags(reserve_percent=reserve_percent, group='cells') + return state.CellStateManager() - mgr = state.CellStateManager() - my_state = mgr.get_my_state() + def _capacity(self, reserve_percent): + state_manager = self._get_state_manager(reserve_percent) + my_state = state_manager.get_my_state() return my_state.capacities + + +class TestCellsGetCapacity(TestCellsStateManager): + def setUp(self): + super(TestCellsGetCapacity, self).setUp() + self.capacities = {"ram_free": 1234} + self.state_manager = self._get_state_manager() + cell = models.Cell(name="cell_name") + other_cell = models.Cell(name="other_cell_name") + cell.capacities = self.capacities + other_cell.capacities = self.capacities + self.stubs.Set(self.state_manager, 'child_cells', + {"cell_name": cell, + "other_cell_name": other_cell}) + + def test_get_cell_capacity_for_all_cells(self): + self.stubs.Set(self.state_manager.my_cell_state, 'capacities', + self.capacities) + capacities = self.state_manager.get_capacities() + self.assertEqual({"ram_free": 3702}, capacities) + + def test_get_cell_capacity_for_the_parent_cell(self): + self.stubs.Set(self.state_manager.my_cell_state, 'capacities', + self.capacities) + capacities = self.state_manager.\ + get_capacities(self.state_manager.my_cell_state.name) + self.assertEqual({"ram_free": 3702}, capacities) + + def test_get_cell_capacity_for_a_cell(self): + self.assertEqual(self.capacities, + self.state_manager.get_capacities(cell_name="cell_name")) + + def test_get_cell_capacity_for_non_existing_cell(self): + self.assertRaises(exception.CellNotFound, + self.state_manager.get_capacities, + cell_name="invalid_cell_name") diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index af539c1e3e64..0ce9829a7f22 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -160,6 +160,14 @@ "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1", "updated": "%(timestamp)s" }, + { + "alias": "os-cell-capacities", + "description": "%(text)s", + "links": [], + "name": "CellCapacities", + "namespace": "http://docs.openstack.org/compute/ext/cell_capacities/api/v1.1", + "updated": "%(timestamp)s" + }, { "alias": "os-certificates", "description": "%(text)s", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index 205c5bc4dace..9f31994186c3 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -60,6 +60,9 @@ %(text)s + + %(text)s + %(text)s diff --git a/nova/tests/integrated/api_samples/os-cell-capacities/cells-capacities-resp.json.tpl b/nova/tests/integrated/api_samples/os-cell-capacities/cells-capacities-resp.json.tpl new file mode 100644 index 000000000000..b926f8d1df27 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cell-capacities/cells-capacities-resp.json.tpl @@ -0,0 +1,18 @@ +{ + "cell": { + "capacities": { + "ram_free": { + "units_by_mb": { + "8192": 0, "512": 13, "4096": 1, "2048": 3, "16384": 0 + }, + "total_mb": 7680 + }, + "disk_free": { + "units_by_mb": { + "81920": 11, "20480": 46, "40960": 23, "163840": 5, "0": 0 + }, + "total_mb": 1052672 + } + } + } +} diff --git a/nova/tests/integrated/api_samples/os-cell-capacities/cells-capacities-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cell-capacities/cells-capacities-resp.xml.tpl new file mode 100644 index 000000000000..63672b00bd56 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-cell-capacities/cells-capacities-resp.xml.tpl @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index 6cd39177ea8d..7ac0d2633ddc 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -31,6 +31,7 @@ from nova.api.metadata import password from nova.api.openstack.compute.contrib import coverage_ext from nova.api.openstack.compute.contrib import fping # Import extensions to pull in osapi_compute_extension CONF option used below. +from nova.cells import state from nova.cloudpipe import pipelib from nova.compute import api as compute_api from nova.compute import manager as compute_manager @@ -2808,6 +2809,54 @@ class CellsSampleXmlTest(CellsSampleJsonTest): ctype = 'xml' +class CellsCapacitySampleJsonTest(ApiSampleTestBase): + extends_name = ("nova.api.openstack.compute.contrib.cells.Cells") + extension_name = ("nova.api.openstack.compute.contrib." + "cell_capacities.Cell_capacities") + + def setUp(self): + self.flags(enable=True, db_check_interval=-1, group='cells') + super(CellsCapacitySampleJsonTest, self).setUp() + # (navneetk/kaushikc) : Mock cell capacity to avoid the capacity + # being calculated from the compute nodes in the environment + self._mock_cell_capacity() + + def test_get_cell_capacity(self): + state_manager = state.CellStateManager() + my_state = state_manager.get_my_state() + response = self._do_get('os-cells/%s/capacities' % + my_state.name) + subs = self._get_regexes() + return self._verify_response('cells-capacities-resp', + subs, response, 200) + + def test_get_all_cells_capacity(self): + response = self._do_get('os-cells/capacities') + subs = self._get_regexes() + return self._verify_response('cells-capacities-resp', + subs, response, 200) + + def _mock_cell_capacity(self): + self.mox.StubOutWithMock(self.cells.manager.state_manager, + 'get_our_capacities') + response = {"ram_free": + {"units_by_mb": {"8192": 0, "512": 13, + "4096": 1, "2048": 3, "16384": 0}, + "total_mb": 7680}, + "disk_free": + {"units_by_mb": {"81920": 11, "20480": 46, + "40960": 23, "163840": 5, "0": 0}, + "total_mb": 1052672} + } + self.cells.manager.state_manager.get_our_capacities(). \ + AndReturn(response) + self.mox.ReplayAll() + + +class CellsCapacitySampleXmlTest(CellsCapacitySampleJsonTest): + ctype = 'xml' + + class BareMetalNodesJsonTest(ApiSampleTestBase, bm_db_base.BMDBTestCase): extension_name = ('nova.api.openstack.compute.contrib.baremetal_nodes.' 'Baremetal_nodes')