summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-01-15 21:39:16 +0000
committerGerrit Code Review <review@openstack.org>2013-01-15 21:39:16 +0000
commita88152a476de4e5fc04d5b4e3b098527fdfb5a8f (patch)
tree980db596c0bef2d37765c58159a21ee0cab0310b
parentf578fc4e220f4d57aebd6289bbf8b0181c26a066 (diff)
parent6b4ad2d7d2f431c153e9e2a655271932b8b378f0 (diff)
downloadnova-a88152a476de4e5fc04d5b4e3b098527fdfb5a8f.tar.gz
nova-a88152a476de4e5fc04d5b4e3b098527fdfb5a8f.tar.xz
nova-a88152a476de4e5fc04d5b4e3b098527fdfb5a8f.zip
Merge "Cells: Add cells API extension"
-rwxr-xr-xbin/nova-manage8
-rw-r--r--doc/api_samples/all_extensions/extensions-get-resp.json8
-rw-r--r--doc/api_samples/all_extensions/extensions-get-resp.xml6
-rw-r--r--doc/api_samples/os-cells/cells-get-resp.json9
-rw-r--r--doc/api_samples/os-cells/cells-get-resp.xml2
-rw-r--r--doc/api_samples/os-cells/cells-list-empty-resp.json3
-rw-r--r--doc/api_samples/os-cells/cells-list-empty-resp.xml2
-rw-r--r--doc/api_samples/os-cells/cells-list-resp.json39
-rw-r--r--doc/api_samples/os-cells/cells-list-resp.xml8
-rw-r--r--doc/api_samples/os-hosts/hosts-list-resp.json5
-rw-r--r--doc/api_samples/os-hosts/hosts-list-resp.xml3
-rw-r--r--etc/nova/policy.json1
-rw-r--r--nova/api/openstack/compute/contrib/cells.py303
-rw-r--r--nova/cells/manager.py13
-rw-r--r--nova/cells/messaging.py34
-rw-r--r--nova/cells/rpcapi.py19
-rw-r--r--nova/cells/state.py13
-rw-r--r--nova/db/api.py12
-rw-r--r--nova/db/sqlalchemy/api.py30
-rw-r--r--nova/exception.py2
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_cells.py396
-rw-r--r--nova/tests/cells/test_cells_manager.py26
-rw-r--r--nova/tests/cells/test_cells_messaging.py46
-rw-r--r--nova/tests/cells/test_cells_rpcapi.py20
-rw-r--r--nova/tests/fake_policy.py1
-rw-r--r--nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl8
-rw-r--r--nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl3
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl9
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl2
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl4
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl2
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl39
-rw-r--r--nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl8
-rw-r--r--nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl11
-rw-r--r--nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl1
-rw-r--r--nova/tests/integrated/integrated_helpers.py5
-rw-r--r--nova/tests/integrated/test_api_samples.py62
37 files changed, 1127 insertions, 36 deletions
diff --git a/bin/nova-manage b/bin/nova-manage
index c783c304b..4f3d889ea 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -1056,11 +1056,11 @@ class CellCommands(object):
ctxt = context.get_admin_context()
db.cell_create(ctxt, values)
- @args('--cell_id', dest='cell_id', metavar='<cell_id>',
- help='ID of the cell to delete')
- def delete(self, cell_id):
+ @args('--cell_name', dest='cell_name', metavar='<cell_name>',
+ help='Name of the cell to delete')
+ def delete(self, cell_name):
ctxt = context.get_admin_context()
- db.cell_delete(ctxt, cell_id)
+ db.cell_delete(ctxt, cell_name)
def list(self):
ctxt = context.get_admin_context()
diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json
index 25d077f27..bd002c080 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.json
+++ b/doc/api_samples/all_extensions/extensions-get-resp.json
@@ -89,6 +89,14 @@
"updated": "2012-08-09T00:00:00+00:00"
},
{
+ "alias": "os-cells",
+ "description": "Enables cells-related functionality such as adding neighbor cells,\n listing neighbor cells, and getting the capabilities of the local cell.\n ",
+ "links": [],
+ "name": "Cells",
+ "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1",
+ "updated": "2011-09-21T00:00:00+00:00"
+ },
+ {
"alias": "os-certificates",
"description": "Certificates support.",
"links": [],
diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml
index b66c3dbe7..ebb1c4302 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.xml
+++ b/doc/api_samples/all_extensions/extensions-get-resp.xml
@@ -37,6 +37,12 @@
<extension alias="os-availability-zone" updated="2012-08-09T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1" name="AvailabilityZone">
<description>Add availability_zone to the Create Server v1.1 API.</description>
</extension>
+ <extension alias="os-cells" updated="2011-09-21T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells">
+ <description>Enables cells-related functionality such as adding child cells,
+ listing child cells, getting the capabilities of the local cell,
+ and returning build plans to parent cells' schedulers
+ </description>
+ </extension>
<extension alias="os-certificates" updated="2012-01-19T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/certificates/api/v1.1" name="Certificates">
<description>Certificates support.</description>
</extension>
diff --git a/doc/api_samples/os-cells/cells-get-resp.json b/doc/api_samples/os-cells/cells-get-resp.json
new file mode 100644
index 000000000..62eb8ec31
--- /dev/null
+++ b/doc/api_samples/os-cells/cells-get-resp.json
@@ -0,0 +1,9 @@
+{
+ "cell": {
+ "name": "cell3",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "child",
+ "username": "username3"
+ }
+} \ No newline at end of file
diff --git a/doc/api_samples/os-cells/cells-get-resp.xml b/doc/api_samples/os-cells/cells-get-resp.xml
new file mode 100644
index 000000000..12256a5bd
--- /dev/null
+++ b/doc/api_samples/os-cells/cells-get-resp.xml
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<cell xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" username="username3" rpc_host="None" type="child" name="cell3" rpc_port="None"/> \ No newline at end of file
diff --git a/doc/api_samples/os-cells/cells-list-empty-resp.json b/doc/api_samples/os-cells/cells-list-empty-resp.json
new file mode 100644
index 000000000..5325a4e85
--- /dev/null
+++ b/doc/api_samples/os-cells/cells-list-empty-resp.json
@@ -0,0 +1,3 @@
+{
+ "cells": []
+} \ No newline at end of file
diff --git a/doc/api_samples/os-cells/cells-list-empty-resp.xml b/doc/api_samples/os-cells/cells-list-empty-resp.xml
new file mode 100644
index 000000000..6ac77b4bd
--- /dev/null
+++ b/doc/api_samples/os-cells/cells-list-empty-resp.xml
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/> \ No newline at end of file
diff --git a/doc/api_samples/os-cells/cells-list-resp.json b/doc/api_samples/os-cells/cells-list-resp.json
new file mode 100644
index 000000000..97ea4c6dd
--- /dev/null
+++ b/doc/api_samples/os-cells/cells-list-resp.json
@@ -0,0 +1,39 @@
+{
+ "cells": [
+ {
+ "name": "cell1",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "child",
+ "username": "username1"
+ },
+ {
+ "name": "cell3",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "child",
+ "username": "username3"
+ },
+ {
+ "name": "cell5",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "child",
+ "username": "username5"
+ },
+ {
+ "name": "cell2",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "parent",
+ "username": "username2"
+ },
+ {
+ "name": "cell4",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "parent",
+ "username": "username4"
+ }
+ ]
+} \ No newline at end of file
diff --git a/doc/api_samples/os-cells/cells-list-resp.xml b/doc/api_samples/os-cells/cells-list-resp.xml
new file mode 100644
index 000000000..7d697bb91
--- /dev/null
+++ b/doc/api_samples/os-cells/cells-list-resp.xml
@@ -0,0 +1,8 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0">
+ <cell username="username1" rpc_host="None" type="child" name="cell1" rpc_port="None"/>
+ <cell username="username3" rpc_host="None" type="child" name="cell3" rpc_port="None"/>
+ <cell username="username5" rpc_host="None" type="child" name="cell5" rpc_port="None"/>
+ <cell username="username2" rpc_host="None" type="parent" name="cell2" rpc_port="None"/>
+ <cell username="username4" rpc_host="None" type="parent" name="cell4" rpc_port="None"/>
+</cells> \ No newline at end of file
diff --git a/doc/api_samples/os-hosts/hosts-list-resp.json b/doc/api_samples/os-hosts/hosts-list-resp.json
index 5a963c602..0c4126a7e 100644
--- a/doc/api_samples/os-hosts/hosts-list-resp.json
+++ b/doc/api_samples/os-hosts/hosts-list-resp.json
@@ -24,6 +24,11 @@
"host_name": "6e48bfe1a3304b7b86154326328750ae",
"service": "conductor",
"zone": "internal"
+ },
+ {
+ "host_name": "39f55087a1024d1380755951c945ca69",
+ "service": "cells",
+ "zone": "internal"
}
]
}
diff --git a/doc/api_samples/os-hosts/hosts-list-resp.xml b/doc/api_samples/os-hosts/hosts-list-resp.xml
index 8266a5d49..9a99c577a 100644
--- a/doc/api_samples/os-hosts/hosts-list-resp.xml
+++ b/doc/api_samples/os-hosts/hosts-list-resp.xml
@@ -5,4 +5,5 @@
<host host_name="2d1bdd671b5d41fd89dec74be5770c63" service="network"/>
<host host_name="7c2dd5ecb7494dd1bf4240b7f7f9bf3a" service="scheduler"/>
<host host_name="f9c273d8e03141a2a01def0ad18e5be4" service="conductor"/>
-</hosts> \ No newline at end of file
+ <host host_name="2b893569cd824b979bd80a2c94570a1f" service="cells"/>
+</hosts>
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index 04766371e..fd1f9c2e0 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -29,6 +29,7 @@
"compute_extension:admin_actions:migrate": "rule:admin_api",
"compute_extension:aggregates": "rule:admin_api",
"compute_extension:agents": "rule:admin_api",
+ "compute_extension:cells": "rule:admin_api",
"compute_extension:certificates": "",
"compute_extension:cloudpipe": "rule:admin_api",
"compute_extension:cloudpipe_update": "rule:admin_api",
diff --git a/nova/api/openstack/compute/contrib/cells.py b/nova/api/openstack/compute/contrib/cells.py
new file mode 100644
index 000000000..03e2e4ca2
--- /dev/null
+++ b/nova/api/openstack/compute/contrib/cells.py
@@ -0,0 +1,303 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011-2012 OpenStack LLC.
+# 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 cells extension."""
+from xml.dom import minidom
+from xml.parsers import expat
+
+from webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.cells import rpcapi as cells_rpcapi
+from nova.compute import api as compute
+from nova import db
+from nova import exception
+from nova.openstack.common import cfg
+from nova.openstack.common import log as logging
+from nova.openstack.common import timeutils
+
+
+LOG = logging.getLogger(__name__)
+CONF = cfg.CONF
+CONF.import_opt('name', 'nova.cells.opts', group='cells')
+CONF.import_opt('capabilities', 'nova.cells.opts', group='cells')
+
+authorize = extensions.extension_authorizer('compute', 'cells')
+
+
+def make_cell(elem):
+ elem.set('name')
+ elem.set('username')
+ elem.set('type')
+ elem.set('rpc_host')
+ elem.set('rpc_port')
+
+ caps = xmlutil.SubTemplateElement(elem, 'capabilities',
+ selector='capabilities')
+ cap = xmlutil.SubTemplateElement(caps, xmlutil.Selector(0),
+ selector=xmlutil.get_items)
+ cap.text = 1
+
+
+cell_nsmap = {None: wsgi.XMLNS_V10}
+
+
+class CellTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('cell', selector='cell')
+ make_cell(root)
+ return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap)
+
+
+class CellsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('cells')
+ elem = xmlutil.SubTemplateElement(root, 'cell', selector='cells')
+ make_cell(elem)
+ return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap)
+
+
+class CellDeserializer(wsgi.XMLDeserializer):
+ """Deserializer to handle xml-formatted cell create requests."""
+
+ def _extract_capabilities(self, cap_node):
+ caps = {}
+ for cap in cap_node.childNodes:
+ cap_name = cap.tagName
+ caps[cap_name] = self.extract_text(cap)
+ return caps
+
+ def _extract_cell(self, node):
+ cell = {}
+ cell_node = self.find_first_child_named(node, 'cell')
+
+ extract_fns = {'capabilities': self._extract_capabilities}
+
+ for child in cell_node.childNodes:
+ name = child.tagName
+ extract_fn = extract_fns.get(name, self.extract_text)
+ cell[name] = extract_fn(child)
+ return cell
+
+ def default(self, string):
+ """Deserialize an xml-formatted cell create request."""
+ try:
+ node = minidom.parseString(string)
+ except expat.ExpatError:
+ msg = _("cannot understand XML")
+ raise exception.MalformedRequestBody(reason=msg)
+
+ return {'body': {'cell': self._extract_cell(node)}}
+
+
+def _filter_keys(item, keys):
+ """
+ Filters all model attributes except for keys
+ item is a dict
+
+ """
+ return dict((k, v) for k, v in item.iteritems() if k in keys)
+
+
+def _scrub_cell(cell, detail=False):
+ keys = ['name', 'username', 'rpc_host', 'rpc_port']
+ if detail:
+ keys.append('capabilities')
+
+ cell_info = _filter_keys(cell, keys)
+ cell_info['type'] = 'parent' if cell['is_parent'] else 'child'
+ return cell_info
+
+
+class Controller(object):
+ """Controller for Cell resources."""
+
+ def __init__(self):
+ self.compute_api = compute.API()
+ self.cells_rpcapi = cells_rpcapi.CellsAPI()
+
+ def _get_cells(self, ctxt, req, detail=False):
+ """Return all cells."""
+ # Ask the CellsManager for the most recent data
+ items = self.cells_rpcapi.get_cell_info_for_neighbors(ctxt)
+ items = common.limited(items, req)
+ items = [_scrub_cell(item, detail=detail) for item in items]
+ return dict(cells=items)
+
+ @wsgi.serializers(xml=CellsTemplate)
+ def index(self, req):
+ """Return all cells in brief."""
+ ctxt = req.environ['nova.context']
+ authorize(ctxt)
+ return self._get_cells(ctxt, req)
+
+ @wsgi.serializers(xml=CellsTemplate)
+ def detail(self, req):
+ """Return all cells in detail."""
+ ctxt = req.environ['nova.context']
+ authorize(ctxt)
+ return self._get_cells(ctxt, req, detail=True)
+
+ @wsgi.serializers(xml=CellTemplate)
+ def info(self, req):
+ """Return name and capabilities for this cell."""
+ context = req.environ['nova.context']
+ authorize(context)
+ cell_capabs = {}
+ my_caps = CONF.cells.capabilities
+ for cap in my_caps:
+ key, value = cap.split('=')
+ cell_capabs[key] = value
+ cell = {'name': CONF.cells.name,
+ 'type': 'self',
+ 'rpc_host': None,
+ 'rpc_port': 0,
+ 'username': None,
+ 'capabilities': cell_capabs}
+ return dict(cell=cell)
+
+ @wsgi.serializers(xml=CellTemplate)
+ def show(self, req, id):
+ """Return data about the given cell name. 'id' is a cell name."""
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ cell = db.cell_get(context, id)
+ except exception.CellNotFound:
+ raise exc.HTTPNotFound()
+ return dict(cell=_scrub_cell(cell))
+
+ def delete(self, req, id):
+ """Delete a child or parent cell entry. 'id' is a cell name."""
+ context = req.environ['nova.context']
+ authorize(context)
+ num_deleted = db.cell_delete(context, id)
+ if num_deleted == 0:
+ raise exc.HTTPNotFound()
+ return {}
+
+ def _validate_cell_name(self, cell_name):
+ """Validate cell name is not empty and doesn't contain '!' or '.'."""
+ if not cell_name:
+ msg = _("Cell name cannot be empty")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ if '!' in cell_name or '.' in cell_name:
+ msg = _("Cell name cannot contain '!' or '.'")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ def _validate_cell_type(self, cell_type):
+ """Validate cell_type is 'parent' or 'child'."""
+ if cell_type not in ['parent', 'child']:
+ msg = _("Cell type must be 'parent' or 'child'")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ def _convert_cell_type(self, cell):
+ """Convert cell['type'] to is_parent boolean."""
+ if 'type' in cell:
+ self._validate_cell_type(cell['type'])
+ cell['is_parent'] = cell['type'] == 'parent'
+ del cell['type']
+ else:
+ cell['is_parent'] = False
+
+ @wsgi.serializers(xml=CellTemplate)
+ @wsgi.deserializers(xml=CellDeserializer)
+ def create(self, req, body):
+ """Create a child cell entry."""
+ context = req.environ['nova.context']
+ authorize(context)
+ if 'cell' not in body:
+ msg = _("No cell information in request")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ cell = body['cell']
+ if 'name' not in cell:
+ msg = _("No cell name in request")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ self._validate_cell_name(cell['name'])
+ self._convert_cell_type(cell)
+ cell = db.cell_create(context, cell)
+ return dict(cell=_scrub_cell(cell))
+
+ @wsgi.serializers(xml=CellTemplate)
+ @wsgi.deserializers(xml=CellDeserializer)
+ def update(self, req, id, body):
+ """Update a child cell entry. 'id' is the cell name to update."""
+ context = req.environ['nova.context']
+ authorize(context)
+ if 'cell' not in body:
+ msg = _("No cell information in request")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ cell = body['cell']
+ cell.pop('id', None)
+ if 'name' in cell:
+ self._validate_cell_name(cell['name'])
+ self._convert_cell_type(cell)
+ try:
+ cell = db.cell_update(context, id, cell)
+ except exception.CellNotFound:
+ raise exc.HTTPNotFound()
+ return dict(cell=_scrub_cell(cell))
+
+ def sync_instances(self, req, body):
+ """Tell all cells to sync instance info."""
+ context = req.environ['nova.context']
+ authorize(context)
+ project_id = body.pop('project_id', None)
+ deleted = body.pop('deleted', False)
+ updated_since = body.pop('updated_since', None)
+ if body:
+ msg = _("Only 'updated_since' and 'project_id' are understood.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ if updated_since:
+ try:
+ timeutils.parse_isotime(updated_since)
+ except ValueError:
+ msg = _('Invalid changes-since value')
+ raise exc.HTTPBadRequest(explanation=msg)
+ self.cells_rpcapi.sync_instances(context, project_id=project_id,
+ updated_since=updated_since, deleted=deleted)
+
+
+class Cells(extensions.ExtensionDescriptor):
+ """Enables cells-related functionality such as adding neighbor cells,
+ listing neighbor cells, and getting the capabilities of the local cell.
+ """
+
+ name = "Cells"
+ alias = "os-cells"
+ namespace = "http://docs.openstack.org/compute/ext/cells/api/v1.1"
+ updated = "2011-09-21T00:00:00+00:00"
+
+ def get_resources(self):
+ coll_actions = {
+ 'detail': 'GET',
+ 'info': 'GET',
+ 'sync_instances': 'POST',
+ }
+
+ res = extensions.ResourceExtension('os-cells',
+ Controller(), collection_actions=coll_actions)
+ return [res]
diff --git a/nova/cells/manager.py b/nova/cells/manager.py
index 0942bae28..133946794 100644
--- a/nova/cells/manager.py
+++ b/nova/cells/manager.py
@@ -65,7 +65,7 @@ class CellsManager(manager.Manager):
Scheduling requests get passed to the scheduler class.
"""
- RPC_API_VERSION = '1.0'
+ RPC_API_VERSION = '1.1'
def __init__(self, *args, **kwargs):
# Mostly for tests.
@@ -186,6 +186,10 @@ class CellsManager(manager.Manager):
self.msg_runner.schedule_run_instance(ctxt, our_cell,
host_sched_kwargs)
+ def get_cell_info_for_neighbors(self, _ctxt):
+ """Return cell information for our neighbor cells."""
+ return self.state_manager.get_cell_info_for_neighbors()
+
def run_compute_api_method(self, ctxt, cell_name, method_info, call):
"""Call a compute API method in a specific cell."""
response = self.msg_runner.run_compute_api_method(ctxt,
@@ -218,3 +222,10 @@ class CellsManager(manager.Manager):
def bw_usage_update_at_top(self, ctxt, bw_update_info):
"""Update bandwidth usage at top level cell."""
self.msg_runner.bw_usage_update_at_top(ctxt, bw_update_info)
+
+ def sync_instances(self, ctxt, project_id, updated_since, deleted):
+ """Force a sync of all instances, potentially by project_id,
+ and potentially since a certain date/time.
+ """
+ self.msg_runner.sync_instances(ctxt, project_id, updated_since,
+ deleted)
diff --git a/nova/cells/messaging.py b/nova/cells/messaging.py
index 56d521892..34ca74855 100644
--- a/nova/cells/messaging.py
+++ b/nova/cells/messaging.py
@@ -27,6 +27,7 @@ import sys
from eventlet import queue
from nova.cells import state as cells_state
+from nova.cells import utils as cells_utils
from nova import compute
from nova import context
from nova.db import base
@@ -37,6 +38,7 @@ from nova.openstack.common import importutils
from nova.openstack.common import jsonutils
from nova.openstack.common import log as logging
from nova.openstack.common.rpc import common as rpc_common
+from nova.openstack.common import timeutils
from nova.openstack.common import uuidutils
from nova import utils
@@ -778,6 +780,26 @@ class _BroadcastMessageMethods(_BaseMessageMethods):
return
self.db.bw_usage_update(message.ctxt, **bw_update_info)
+ def _sync_instance(self, ctxt, instance):
+ if instance['deleted']:
+ self.msg_runner.instance_destroy_at_top(ctxt, instance)
+ else:
+ self.msg_runner.instance_update_at_top(ctxt, instance)
+
+ def sync_instances(self, message, project_id, updated_since, deleted,
+ **kwargs):
+ projid_str = project_id is None and "<all>" or project_id
+ since_str = updated_since is None and "<all>" or updated_since
+ LOG.info(_("Forcing a sync of instances, project_id="
+ "%(projid_str)s, updated_since=%(since_str)s"), locals())
+ if updated_since is not None:
+ updated_since = timeutils.parse_isotime(updated_since)
+ instances = cells_utils.get_instances_to_sync(message.ctxt,
+ updated_since=updated_since, project_id=project_id,
+ deleted=deleted)
+ for instance in instances:
+ self._sync_instance(message.ctxt, instance)
+
_CELL_MESSAGE_TYPE_TO_MESSAGE_CLS = {'targeted': _TargetedMessage,
'broadcast': _BroadcastMessage,
@@ -1004,6 +1026,18 @@ class MessageRunner(object):
'up', run_locally=False)
message.process()
+ def sync_instances(self, ctxt, project_id, updated_since, deleted):
+ """Force a sync of all instances, potentially by project_id,
+ and potentially since a certain date/time.
+ """
+ method_kwargs = dict(project_id=project_id,
+ updated_since=updated_since,
+ deleted=deleted)
+ message = _BroadcastMessage(self, ctxt, 'sync_instances',
+ method_kwargs, 'down',
+ run_locally=False)
+ message.process()
+
@staticmethod
def get_message_types():
return _CELL_MESSAGE_TYPE_TO_MESSAGE_CLS.keys()
diff --git a/nova/cells/rpcapi.py b/nova/cells/rpcapi.py
index 8ce298829..0ab4fc352 100644
--- a/nova/cells/rpcapi.py
+++ b/nova/cells/rpcapi.py
@@ -40,6 +40,7 @@ class CellsAPI(rpc_proxy.RpcProxy):
API version history:
1.0 - Initial version.
+ 1.1 - Adds get_cell_info_for_neighbors() and sync_instances()
'''
BASE_RPC_API_VERSION = '1.0'
@@ -136,3 +137,21 @@ class CellsAPI(rpc_proxy.RpcProxy):
'info_cache': iicache}
self.cast(ctxt, self.make_msg('instance_update_at_top',
instance=instance))
+
+ def get_cell_info_for_neighbors(self, ctxt):
+ """Get information about our neighbor cells from the manager."""
+ if not CONF.cells.enable:
+ return []
+ return self.call(ctxt, self.make_msg('get_cell_info_for_neighbors'),
+ version='1.1')
+
+ def sync_instances(self, ctxt, project_id=None, updated_since=None,
+ deleted=False):
+ """Ask all cells to sync instance data."""
+ if not CONF.cells.enable:
+ return
+ return self.cast(ctxt, self.make_msg('sync_instances',
+ project_id=project_id,
+ updated_since=updated_since,
+ deleted=deleted),
+ version='1.1')
diff --git a/nova/cells/state.py b/nova/cells/state.py
index 345c44ca9..e3886bedb 100644
--- a/nova/cells/state.py
+++ b/nova/cells/state.py
@@ -75,8 +75,8 @@ class CellState(object):
def get_cell_info(self):
"""Return subset of cell information for OS API use."""
- db_fields_to_return = ['id', 'is_parent', 'weight_scale',
- 'weight_offset', 'username', 'rpc_host', 'rpc_port']
+ db_fields_to_return = ['is_parent', 'weight_scale', 'weight_offset',
+ 'username', 'rpc_host', 'rpc_port']
cell_info = dict(name=self.name, capabilities=self.capabilities)
if self.db_info:
for field in db_fields_to_return:
@@ -267,6 +267,15 @@ class CellStateManager(base.Base):
self._update_our_capacity(ctxt)
@sync_from_db
+ def get_cell_info_for_neighbors(self):
+ """Return cell information for all neighbor cells."""
+ cell_list = [cell.get_cell_info()
+ for cell in self.child_cells.itervalues()]
+ cell_list.extend([cell.get_cell_info()
+ for cell in self.parent_cells.itervalues()])
+ return cell_list
+
+ @sync_from_db
def get_my_state(self):
"""Return information for my (this) cell."""
return self.my_cell_state
diff --git a/nova/db/api.py b/nova/db/api.py
index d7d9bd0d2..ecfcfab15 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -1360,19 +1360,19 @@ def cell_create(context, values):
return IMPL.cell_create(context, values)
-def cell_update(context, cell_id, values):
+def cell_update(context, cell_name, values):
"""Update a child Cell entry."""
- return IMPL.cell_update(context, cell_id, values)
+ return IMPL.cell_update(context, cell_name, values)
-def cell_delete(context, cell_id):
+def cell_delete(context, cell_name):
"""Delete a child Cell."""
- return IMPL.cell_delete(context, cell_id)
+ return IMPL.cell_delete(context, cell_name)
-def cell_get(context, cell_id):
+def cell_get(context, cell_name):
"""Get a specific child Cell."""
- return IMPL.cell_get(context, cell_id)
+ return IMPL.cell_get(context, cell_name)
def cell_get_all(context):
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 3fdfd53c8..038a47ca1 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -3719,34 +3719,30 @@ def cell_create(context, values):
return cell
-def _cell_get_by_id_query(context, cell_id, session=None):
- return model_query(context, models.Cell, session=session).\
- filter_by(id=cell_id)
+def _cell_get_by_name_query(context, cell_name, session=None):
+ return model_query(context, models.Cell,
+ session=session).filter_by(name=cell_name)
@require_admin_context
-def cell_update(context, cell_id, values):
- cell = cell_get(context, cell_id)
- cell.update(values)
- cell.save()
+def cell_update(context, cell_name, values):
+ session = get_session()
+ with session.begin():
+ cell = _cell_get_by_name_query(context, cell_name, session=session)
+ cell.update(values)
return cell
@require_admin_context
-def cell_delete(context, cell_id):
- session = get_session()
- with session.begin():
- return _cell_get_by_id_query(context, cell_id, session=session).\
- delete()
+def cell_delete(context, cell_name):
+ return _cell_get_by_name_query(context, cell_name).soft_delete()
@require_admin_context
-def cell_get(context, cell_id):
- result = _cell_get_by_id_query(context, cell_id).first()
-
+def cell_get(context, cell_name):
+ result = _cell_get_by_name_query(context, cell_name).first()
if not result:
- raise exception.CellNotFound(cell_id=cell_id)
-
+ raise exception.CellNotFound(cell_name=cell_name)
return result
diff --git a/nova/exception.py b/nova/exception.py
index f96b1eaf3..c1005f866 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -768,7 +768,7 @@ class FlavorAccessNotFound(NotFound):
class CellNotFound(NotFound):
- message = _("Cell %(cell_id)s could not be found.")
+ message = _("Cell %(cell_name)s doesn't exist.")
class CellRoutingInconsistency(NovaException):
diff --git a/nova/tests/api/openstack/compute/contrib/test_cells.py b/nova/tests/api/openstack/compute/contrib/test_cells.py
new file mode 100644
index 000000000..82d469524
--- /dev/null
+++ b/nova/tests/api/openstack/compute/contrib/test_cells.py
@@ -0,0 +1,396 @@
+# Copyright 2011-2012 OpenStack LLC.
+# 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.
+
+import copy
+
+from lxml import etree
+from webob import exc
+
+from nova.api.openstack.compute.contrib import cells as cells_ext
+from nova.api.openstack import xmlutil
+from nova.cells import rpcapi as cells_rpcapi
+from nova import context
+from nova import db
+from nova import exception
+from nova.openstack.common import timeutils
+from nova import test
+from nova.tests.api.openstack import fakes
+
+
+FAKE_CELLS = [
+ dict(id=1, name='cell1', username='bob', is_parent=True,
+ weight_scale=1.0, weight_offset=0.0,
+ rpc_host='r1.example.org', password='xxxx'),
+ dict(id=2, name='cell2', username='alice', is_parent=False,
+ weight_scale=1.0, weight_offset=0.0,
+ rpc_host='r2.example.org', password='qwerty')]
+
+
+FAKE_CAPABILITIES = [
+ {'cap1': '0,1', 'cap2': '2,3'},
+ {'cap3': '4,5', 'cap4': '5,6'}]
+
+
+def fake_db_cell_get(context, cell_name):
+ for cell in FAKE_CELLS:
+ if cell_name == cell['name']:
+ return cell
+ else:
+ raise exception.CellNotFound(cell_name=cell_name)
+
+
+def fake_db_cell_create(context, values):
+ cell = dict(id=1)
+ cell.update(values)
+ return cell
+
+
+def fake_db_cell_update(context, cell_id, values):
+ cell = fake_db_cell_get(context, cell_id)
+ cell.update(values)
+ return cell
+
+
+def fake_cells_api_get_all_cell_info(*args):
+ cells = copy.deepcopy(FAKE_CELLS)
+ del cells[0]['password']
+ del cells[1]['password']
+ for i, cell in enumerate(cells):
+ cell['capabilities'] = FAKE_CAPABILITIES[i]
+ return cells
+
+
+def fake_db_cell_get_all(context):
+ return FAKE_CELLS
+
+
+class CellsTest(test.TestCase):
+ def setUp(self):
+ super(CellsTest, self).setUp()
+ self.stubs.Set(db, 'cell_get', fake_db_cell_get)
+ self.stubs.Set(db, 'cell_get_all', fake_db_cell_get_all)
+ self.stubs.Set(db, 'cell_update', fake_db_cell_update)
+ self.stubs.Set(db, 'cell_create', fake_db_cell_create)
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'get_cell_info_for_neighbors',
+ fake_cells_api_get_all_cell_info)
+
+ self.controller = cells_ext.Controller()
+ self.context = context.get_admin_context()
+
+ def _get_request(self, resource):
+ return fakes.HTTPRequest.blank('/v2/fake/' + resource)
+
+ def test_index(self):
+ req = self._get_request("cells")
+ res_dict = self.controller.index(req)
+
+ self.assertEqual(len(res_dict['cells']), 2)
+ for i, cell in enumerate(res_dict['cells']):
+ self.assertEqual(cell['name'], FAKE_CELLS[i]['name'])
+ self.assertNotIn('capabilitiles', cell)
+ self.assertNotIn('password', cell)
+
+ def test_detail(self):
+ req = self._get_request("cells/detail")
+ res_dict = self.controller.detail(req)
+
+ self.assertEqual(len(res_dict['cells']), 2)
+ for i, cell in enumerate(res_dict['cells']):
+ self.assertEqual(cell['name'], FAKE_CELLS[i]['name'])
+ self.assertEqual(cell['capabilities'], FAKE_CAPABILITIES[i])
+ self.assertNotIn('password', cell)
+
+ def test_show_bogus_cell_raises(self):
+ req = self._get_request("cells/bogus")
+ self.assertRaises(exc.HTTPNotFound, self.controller.show, req, 'bogus')
+
+ def test_get_cell_by_name(self):
+ req = self._get_request("cells/cell1")
+ res_dict = self.controller.show(req, 'cell1')
+ cell = res_dict['cell']
+
+ self.assertEqual(cell['name'], 'cell1')
+ self.assertEqual(cell['rpc_host'], 'r1.example.org')
+ self.assertNotIn('password', cell)
+
+ def test_cell_delete(self):
+ call_info = {'delete_called': 0}
+
+ def fake_db_cell_delete(context, cell_name):
+ self.assertEqual(cell_name, 'cell999')
+ call_info['delete_called'] += 1
+
+ self.stubs.Set(db, 'cell_delete', fake_db_cell_delete)
+
+ req = self._get_request("cells/cell999")
+ self.controller.delete(req, 'cell999')
+ self.assertEqual(call_info['delete_called'], 1)
+
+ def test_delete_bogus_cell_raises(self):
+ req = self._get_request("cells/cell999")
+ req.environ['nova.context'] = self.context
+ self.assertRaises(exc.HTTPNotFound, self.controller.delete, req,
+ 'cell999')
+
+ def test_cell_create_parent(self):
+ body = {'cell': {'name': 'meow',
+ 'username': 'fred',
+ 'password': 'fubar',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent',
+ # Also test this is ignored/stripped
+ 'is_parent': False}}
+
+ req = self._get_request("cells")
+ res_dict = self.controller.create(req, body)
+ cell = res_dict['cell']
+
+ self.assertEqual(cell['name'], 'meow')
+ self.assertEqual(cell['username'], 'fred')
+ self.assertEqual(cell['rpc_host'], 'r3.example.org')
+ self.assertEqual(cell['type'], 'parent')
+ self.assertNotIn('password', cell)
+ self.assertNotIn('is_parent', cell)
+
+ def test_cell_create_child(self):
+ body = {'cell': {'name': 'meow',
+ 'username': 'fred',
+ 'password': 'fubar',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'child'}}
+
+ req = self._get_request("cells")
+ res_dict = self.controller.create(req, body)
+ cell = res_dict['cell']
+
+ self.assertEqual(cell['name'], 'meow')
+ self.assertEqual(cell['username'], 'fred')
+ self.assertEqual(cell['rpc_host'], 'r3.example.org')
+ self.assertEqual(cell['type'], 'child')
+ self.assertNotIn('password', cell)
+ self.assertNotIn('is_parent', cell)
+
+ def test_cell_create_no_name_raises(self):
+ body = {'cell': {'username': 'moocow',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create, req, body)
+
+ def test_cell_create_name_empty_string_raises(self):
+ body = {'cell': {'name': '',
+ 'username': 'fred',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create, req, body)
+
+ def test_cell_create_name_with_bang_raises(self):
+ body = {'cell': {'name': 'moo!cow',
+ 'username': 'fred',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create, req, body)
+
+ def test_cell_create_name_with_dot_raises(self):
+ body = {'cell': {'name': 'moo.cow',
+ 'username': 'fred',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create, req, body)
+
+ def test_cell_create_name_with_invalid_type_raises(self):
+ body = {'cell': {'name': 'moocow',
+ 'username': 'fred',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'invalid'}}
+
+ req = self._get_request("cells")
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create, req, body)
+
+ def test_cell_update(self):
+ body = {'cell': {'username': 'zeb',
+ 'password': 'sneaky'}}
+
+ req = self._get_request("cells/cell1")
+ res_dict = self.controller.update(req, 'cell1', body)
+ cell = res_dict['cell']
+
+ self.assertEqual(cell['name'], 'cell1')
+ self.assertEqual(cell['rpc_host'], FAKE_CELLS[0]['rpc_host'])
+ self.assertEqual(cell['username'], 'zeb')
+ self.assertNotIn('password', cell)
+
+ def test_cell_update_empty_name_raises(self):
+ body = {'cell': {'name': '',
+ 'username': 'zeb',
+ 'password': 'sneaky'}}
+
+ req = self._get_request("cells/cell1")
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.update, req, 'cell1', body)
+
+ def test_cell_update_invalid_type_raises(self):
+ body = {'cell': {'username': 'zeb',
+ 'type': 'invalid',
+ 'password': 'sneaky'}}
+
+ req = self._get_request("cells/cell1")
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.update, req, 'cell1', body)
+
+ def test_cell_info(self):
+ caps = ['cap1=a;b', 'cap2=c;d']
+ self.flags(name='darksecret', capabilities=caps, group='cells')
+
+ req = self._get_request("cells/info")
+ res_dict = self.controller.info(req)
+ cell = res_dict['cell']
+ cell_caps = cell['capabilities']
+
+ self.assertEqual(cell['name'], 'darksecret')
+ self.assertEqual(cell_caps['cap1'], 'a;b')
+ self.assertEqual(cell_caps['cap2'], 'c;d')
+
+ def test_sync_instances(self):
+ call_info = {}
+
+ def sync_instances(self, context, **kwargs):
+ call_info['project_id'] = kwargs.get('project_id')
+ call_info['updated_since'] = kwargs.get('updated_since')
+
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'sync_instances', sync_instances)
+
+ req = self._get_request("cells/sync_instances")
+ body = {}
+ self.controller.sync_instances(req, body=body)
+ self.assertEqual(call_info['project_id'], None)
+ self.assertEqual(call_info['updated_since'], None)
+
+ body = {'project_id': 'test-project'}
+ self.controller.sync_instances(req, body=body)
+ self.assertEqual(call_info['project_id'], 'test-project')
+ self.assertEqual(call_info['updated_since'], None)
+
+ expected = timeutils.utcnow().isoformat()
+ if not expected.endswith("+00:00"):
+ expected += "+00:00"
+
+ body = {'updated_since': expected}
+ self.controller.sync_instances(req, body=body)
+ self.assertEqual(call_info['project_id'], None)
+ self.assertEqual(call_info['updated_since'], expected)
+
+ body = {'updated_since': 'skjdfkjsdkf'}
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.sync_instances, req, body=body)
+
+ body = {'foo': 'meow'}
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.sync_instances, req, body=body)
+
+
+class TestCellsXMLSerializer(test.TestCase):
+ def test_multiple_cells(self):
+ fixture = {'cells': fake_cells_api_get_all_cell_info()}
+
+ serializer = cells_ext.CellsTemplate()
+ output = serializer.serialize(fixture)
+ res_tree = etree.XML(output)
+
+ self.assertEqual(res_tree.tag, '{%s}cells' % xmlutil.XMLNS_V10)
+ self.assertEqual(len(res_tree), 2)
+ self.assertEqual(res_tree[0].tag, '{%s}cell' % xmlutil.XMLNS_V10)
+ self.assertEqual(res_tree[1].tag, '{%s}cell' % xmlutil.XMLNS_V10)
+
+ def test_single_cell_with_caps(self):
+ cell = {'id': 1,
+ 'name': 'darksecret',
+ 'username': 'meow',
+ 'capabilities': {'cap1': 'a;b',
+ 'cap2': 'c;d'}}
+ fixture = {'cell': cell}
+
+ serializer = cells_ext.CellTemplate()
+ output = serializer.serialize(fixture)
+ res_tree = etree.XML(output)
+
+ self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10)
+ self.assertEqual(res_tree.get('name'), 'darksecret')
+ self.assertEqual(res_tree.get('username'), 'meow')
+ self.assertEqual(res_tree.get('password'), None)
+ self.assertEqual(len(res_tree), 1)
+
+ child = res_tree[0]
+ self.assertEqual(child.tag,
+ '{%s}capabilities' % xmlutil.XMLNS_V10)
+ for elem in child:
+ self.assertIn(elem.tag, ('{%s}cap1' % xmlutil.XMLNS_V10,
+ '{%s}cap2' % xmlutil.XMLNS_V10))
+ if elem.tag == '{%s}cap1' % xmlutil.XMLNS_V10:
+ self.assertEqual(elem.text, 'a;b')
+ elif elem.tag == '{%s}cap2' % xmlutil.XMLNS_V10:
+ self.assertEqual(elem.text, 'c;d')
+
+ def test_single_cell_without_caps(self):
+ cell = {'id': 1,
+ 'username': 'woof',
+ 'name': 'darksecret'}
+ fixture = {'cell': cell}
+
+ serializer = cells_ext.CellTemplate()
+ output = serializer.serialize(fixture)
+ res_tree = etree.XML(output)
+
+ self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10)
+ self.assertEqual(res_tree.get('name'), 'darksecret')
+ self.assertEqual(res_tree.get('username'), 'woof')
+ self.assertEqual(res_tree.get('password'), None)
+ self.assertEqual(len(res_tree), 0)
+
+
+class TestCellsXMLDeserializer(test.TestCase):
+ def test_cell_deserializer(self):
+ caps_dict = {'cap1': 'a;b',
+ 'cap2': 'c;d'}
+ caps_xml = ("<capabilities><cap1>a;b</cap1>"
+ "<cap2>c;d</cap2></capabilities>")
+ expected = {'cell': {'name': 'testcell1',
+ 'type': 'child',
+ 'rpc_host': 'localhost',
+ 'capabilities': caps_dict}}
+ intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<cell><name>testcell1</name><type>child</type>"
+ "<rpc_host>localhost</rpc_host>"
+ "%s</cell>") % caps_xml
+ deserializer = cells_ext.CellDeserializer()
+ result = deserializer.deserialize(intext)
+ self.assertEqual(dict(body=expected), result)
diff --git a/nova/tests/cells/test_cells_manager.py b/nova/tests/cells/test_cells_manager.py
index 72ef3f1f0..ef165f4ed 100644
--- a/nova/tests/cells/test_cells_manager.py
+++ b/nova/tests/cells/test_cells_manager.py
@@ -38,6 +38,21 @@ class CellsManagerClassTestCase(test.TestCase):
self.driver = self.cells_manager.driver
self.ctxt = 'fake_context'
+ def _get_fake_responses(self):
+ responses = []
+ expected_responses = []
+ for x in xrange(1, 4):
+ responses.append(messaging.Response('cell%s' % x, x, False))
+ expected_responses.append(('cell%s' % x, x))
+ return expected_responses, responses
+
+ def test_get_cell_info_for_neighbors(self):
+ self.mox.StubOutWithMock(self.cells_manager.state_manager,
+ 'get_cell_info_for_neighbors')
+ self.cells_manager.state_manager.get_cell_info_for_neighbors()
+ self.mox.ReplayAll()
+ self.cells_manager.get_cell_info_for_neighbors(self.ctxt)
+
def test_post_start_hook_child_cell(self):
self.mox.StubOutWithMock(self.driver, 'start_consumers')
self.mox.StubOutWithMock(context, 'get_admin_context')
@@ -211,3 +226,14 @@ class CellsManagerClassTestCase(test.TestCase):
# Now the last 1 and the first 1
self.assertEqual(call_info['sync_instances'],
[instances[-1], instances[0]])
+
+ def test_sync_instances(self):
+ self.mox.StubOutWithMock(self.msg_runner,
+ 'sync_instances')
+ self.msg_runner.sync_instances(self.ctxt, 'fake-project',
+ 'fake-time', 'fake-deleted')
+ self.mox.ReplayAll()
+ self.cells_manager.sync_instances(self.ctxt,
+ project_id='fake-project',
+ updated_since='fake-time',
+ deleted='fake-deleted')
diff --git a/nova/tests/cells/test_cells_messaging.py b/nova/tests/cells/test_cells_messaging.py
index 9973716f6..da45721ed 100644
--- a/nova/tests/cells/test_cells_messaging.py
+++ b/nova/tests/cells/test_cells_messaging.py
@@ -14,11 +14,14 @@
"""
Tests For Cells Messaging module
"""
+import mox
from nova.cells import messaging
+from nova.cells import utils as cells_utils
from nova import context
from nova import exception
from nova.openstack.common import cfg
+from nova.openstack.common import timeutils
from nova import test
from nova.tests.cells import fakes
@@ -912,3 +915,46 @@ class CellsBroadcastMethodsTestCase(test.TestCase):
self.src_msg_runner.bw_usage_update_at_top(self.ctxt,
fake_bw_update_info)
+
+ def test_sync_instances(self):
+ # Reset this, as this is a broadcast down.
+ self._setup_attrs(up=False)
+ project_id = 'fake_project_id'
+ updated_since_raw = 'fake_updated_since_raw'
+ updated_since_parsed = 'fake_updated_since_parsed'
+ deleted = 'fake_deleted'
+
+ instance1 = dict(uuid='fake_uuid1', deleted=False)
+ instance2 = dict(uuid='fake_uuid2', deleted=True)
+ fake_instances = [instance1, instance2]
+
+ self.mox.StubOutWithMock(self.tgt_msg_runner,
+ 'instance_update_at_top')
+ self.mox.StubOutWithMock(self.tgt_msg_runner,
+ 'instance_destroy_at_top')
+
+ self.mox.StubOutWithMock(timeutils, 'parse_isotime')
+ self.mox.StubOutWithMock(cells_utils, 'get_instances_to_sync')
+
+ # Middle cell.
+ timeutils.parse_isotime(updated_since_raw).AndReturn(
+ updated_since_parsed)
+ cells_utils.get_instances_to_sync(self.ctxt,
+ updated_since=updated_since_parsed,
+ project_id=project_id,
+ deleted=deleted).AndReturn([])
+
+ # Bottom/Target cell
+ timeutils.parse_isotime(updated_since_raw).AndReturn(
+ updated_since_parsed)
+ cells_utils.get_instances_to_sync(self.ctxt,
+ updated_since=updated_since_parsed,
+ project_id=project_id,
+ deleted=deleted).AndReturn(fake_instances)
+ self.tgt_msg_runner.instance_update_at_top(self.ctxt, instance1)
+ self.tgt_msg_runner.instance_destroy_at_top(self.ctxt, instance2)
+
+ self.mox.ReplayAll()
+
+ self.src_msg_runner.sync_instances(self.ctxt,
+ project_id, updated_since_raw, deleted)
diff --git a/nova/tests/cells/test_cells_rpcapi.py b/nova/tests/cells/test_cells_rpcapi.py
index b51bfa0c1..5e045aca9 100644
--- a/nova/tests/cells/test_cells_rpcapi.py
+++ b/nova/tests/cells/test_cells_rpcapi.py
@@ -204,3 +204,23 @@ class CellsAPITestCase(test.TestCase):
expected_args = {'bw_update_info': bw_update_info}
self._check_result(call_info, 'bw_usage_update_at_top',
expected_args)
+
+ def test_get_cell_info_for_neighbors(self):
+ call_info = self._stub_rpc_method('call', 'fake_response')
+ result = self.cells_rpcapi.get_cell_info_for_neighbors(
+ self.fake_context)
+ self._check_result(call_info, 'get_cell_info_for_neighbors', {},
+ version='1.1')
+ self.assertEqual(result, 'fake_response')
+
+ def test_sync_instances(self):
+ call_info = self._stub_rpc_method('cast', None)
+ self.cells_rpcapi.sync_instances(self.fake_context,
+ project_id='fake_project', updated_since='fake_time',
+ deleted=True)
+
+ expected_args = {'project_id': 'fake_project',
+ 'updated_since': 'fake_time',
+ 'deleted': True}
+ self._check_result(call_info, 'sync_instances', expected_args,
+ version='1.1')
diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py
index 51f3a3f85..15890cdcd 100644
--- a/nova/tests/fake_policy.py
+++ b/nova/tests/fake_policy.py
@@ -104,6 +104,7 @@ policy_data = """
"compute_extension:admin_actions:migrate": "",
"compute_extension:aggregates": "",
"compute_extension:agents": "",
+ "compute_extension:cells": "",
"compute_extension:certificates": "",
"compute_extension:cloudpipe": "",
"compute_extension:cloudpipe_update": "",
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 3d69fad45..fe0613646 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
@@ -89,6 +89,14 @@
"updated": "%(timestamp)s"
},
{
+ "alias": "os-cells",
+ "description": "%(text)s",
+ "links": [],
+ "name": "Cells",
+ "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1",
+ "updated": "%(timestamp)s"
+ },
+ {
"alias": "os-certificates",
"description": "%(text)s",
"links": [],
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 5953ba704..2051d891a 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
@@ -33,6 +33,9 @@
<extension alias="os-agents" name="Agents" namespace="http://docs.openstack.org/compute/ext/agents/api/v2" updated="%(timestamp)s">
<description>%(text)s</description>
</extension>
+ <extension alias="os-cells" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells">
+ <description>%(text)s</description>
+ </extension>
<extension alias="os-certificates" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/certificates/api/v1.1" name="Certificates">
<description>%(text)s</description>
</extension>
diff --git a/nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl
new file mode 100644
index 000000000..2993b1df8
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.json.tpl
@@ -0,0 +1,9 @@
+{
+ "cell": {
+ "name": "cell3",
+ "username": "username3",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "child"
+ }
+}
diff --git a/nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl
new file mode 100644
index 000000000..d31a674a2
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-cells/cells-get-resp.xml.tpl
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<cell xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" name="cell3" username="username3" rpc_port="None" rpc_host="None" type="child"/>
diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl
new file mode 100644
index 000000000..b16e12cd6
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.json.tpl
@@ -0,0 +1,4 @@
+{
+ "cells": []
+}
+
diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl
new file mode 100644
index 000000000..32fef4f04
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-cells/cells-list-empty-resp.xml.tpl
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/>
diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl
new file mode 100644
index 000000000..3d7a6c207
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.json.tpl
@@ -0,0 +1,39 @@
+{
+ "cells": [
+ {
+ "name": "cell1",
+ "username": "username1",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "child"
+ },
+ {
+ "name": "cell2",
+ "username": "username2",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "parent"
+ },
+ {
+ "name": "cell3",
+ "username": "username3",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "child"
+ },
+ {
+ "name": "cell4",
+ "username": "username4",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "parent"
+ },
+ {
+ "name": "cell5",
+ "username": "username5",
+ "rpc_host": null,
+ "rpc_port": null,
+ "type": "child"
+ }
+ ]
+}
diff --git a/nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl
new file mode 100644
index 000000000..58312201f
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-cells/cells-list-resp.xml.tpl
@@ -0,0 +1,8 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0">
+ <cell name="cell1" username="username1" rpc_port="None" rpc_host="None" type="child"/>
+ <cell name="cell2" username="username2" rpc_port="None" rpc_host="None" type="parent"/>
+ <cell name="cell3" username="username3" rpc_port="None" rpc_host="None" type="child"/>
+ <cell name="cell4" username="username4" rpc_port="None" rpc_host="None" type="parent"/>
+ <cell name="cell5" username="username5" rpc_port="None" rpc_host="None" type="child"/>
+</cells>
diff --git a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl
index eeb191597..504f66f59 100644
--- a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl
+++ b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.json.tpl
@@ -21,9 +21,14 @@
"zone": "internal"
},
{
- "host_name": "%(host_name)s",
- "service": "conductor",
- "zone": "internal"
+ "host_name": "%(host_name)s",
+ "service": "conductor",
+ "zone": "internal"
+ },
+ {
+ "host_name": "%(host_name)s",
+ "service": "cells",
+ "zone": "internal"
}
]
}
diff --git a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl
index 25ef5a299..4e9d3195d 100644
--- a/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl
+++ b/nova/tests/integrated/api_samples/os-hosts/hosts-list-resp.xml.tpl
@@ -5,4 +5,5 @@
<host host_name="%(host_name)s" service="network"/>
<host host_name="%(host_name)s" service="scheduler"/>
<host host_name="%(host_name)s" service="conductor"/>
+ <host host_name="%(host_name)s" service="cells"/>
</hosts>
diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py
index e20d6881b..f17dc025f 100644
--- a/nova/tests/integrated/integrated_helpers.py
+++ b/nova/tests/integrated/integrated_helpers.py
@@ -27,7 +27,7 @@ import nova.image.glance
from nova.openstack.common import cfg
from nova.openstack.common.log import logging
from nova import service
-from nova import test # For the flags
+from nova import test
from nova.tests import fake_crypto
import nova.tests.image.fake
from nova.tests.integrated.api import client
@@ -35,6 +35,8 @@ from nova.tests.integrated.api import client
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
+CONF = cfg.CONF
+CONF.import_opt('manager', 'nova.cells.opts', group='cells')
def generate_random_alphanumeric(length):
@@ -81,6 +83,7 @@ class _IntegratedTestBase(test.TestCase):
self.scheduler = self.start_service('cert')
self.network = self.start_service('network')
self.scheduler = self.start_service('scheduler')
+ self.cells = self.start_service('cells', manager=CONF.cells.manager)
self._start_api_service()
diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py
index 98ac6a230..7c3157872 100644
--- a/nova/tests/integrated/test_api_samples.py
+++ b/nova/tests/integrated/test_api_samples.py
@@ -54,6 +54,8 @@ CONF.import_opt('osapi_compute_extension',
CONF.import_opt('vpn_image_id', 'nova.cloudpipe.pipelib')
CONF.import_opt('osapi_compute_link_prefix', 'nova.api.openstack.common')
CONF.import_opt('osapi_glance_link_prefix', 'nova.api.openstack.common')
+CONF.import_opt('enable', 'nova.cells.opts', group='cells')
+CONF.import_opt('db_check_interval', 'nova.cells.state', group='cells')
LOG = logging.getLogger(__name__)
@@ -2501,3 +2503,63 @@ class QuotaClassesSampleJsonTests(ApiSampleTestBase):
class QuotaClassesSampleXmlTests(QuotaClassesSampleJsonTests):
ctype = "xml"
+
+
+class CellsSampleJsonTest(ApiSampleTestBase):
+ extension_name = "nova.api.openstack.compute.contrib.cells.Cells"
+
+ def setUp(self):
+ # db_check_interval < 0 makes cells manager always hit the DB
+ self.flags(enable=True, db_check_interval=-1, group='cells')
+ super(CellsSampleJsonTest, self).setUp()
+ self._stub_cells()
+
+ def _stub_cells(self, num_cells=5):
+ self.cells = []
+ self.cells_next_id = 1
+
+ def _fake_cell_get_all(context):
+ return self.cells
+
+ def _fake_cell_get(context, cell_name):
+ for cell in self.cells:
+ if cell['name'] == cell_name:
+ return cell
+ raise exception.CellNotFound(cell_name=cell_name)
+
+ for x in xrange(num_cells):
+ cell = models.Cell()
+ our_id = self.cells_next_id
+ self.cells_next_id += 1
+ cell.update({'id': our_id,
+ 'name': 'cell%s' % our_id,
+ 'username': 'username%s' % our_id,
+ 'is_parent': our_id % 2 == 0})
+ self.cells.append(cell)
+
+ self.stubs.Set(db, 'cell_get_all', _fake_cell_get_all)
+ self.stubs.Set(db, 'cell_get', _fake_cell_get)
+
+ def test_cells_empty_list(self):
+ # Override this
+ self._stub_cells(num_cells=0)
+ response = self._do_get('os-cells')
+ self.assertEqual(response.status, 200)
+ subs = self._get_regexes()
+ return self._verify_response('cells-list-empty-resp', subs, response)
+
+ def test_cells_list(self):
+ response = self._do_get('os-cells')
+ self.assertEqual(response.status, 200)
+ subs = self._get_regexes()
+ return self._verify_response('cells-list-resp', subs, response)
+
+ def test_cells_get(self):
+ response = self._do_get('os-cells/cell3')
+ self.assertEqual(response.status, 200)
+ subs = self._get_regexes()
+ return self._verify_response('cells-get-resp', subs, response)
+
+
+class CellsSampleXmlTest(CellsSampleJsonTest):
+ ctype = 'xml'