Merge "Cells: Add cells API extension"

This commit is contained in:
Jenkins 2013-01-15 21:39:16 +00:00 committed by Gerrit Code Review
commit a88152a476
37 changed files with 1127 additions and 36 deletions

View File

@ -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()

View File

@ -88,6 +88,14 @@
"namespace": "http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1",
"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.",

View File

@ -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>

View File

@ -0,0 +1,9 @@
{
"cell": {
"name": "cell3",
"rpc_host": null,
"rpc_port": null,
"type": "child",
"username": "username3"
}
}

View File

@ -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"/>

View File

@ -0,0 +1,3 @@
{
"cells": []
}

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/>

View File

@ -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"
}
]
}

View File

@ -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>

View File

@ -24,6 +24,11 @@
"host_name": "6e48bfe1a3304b7b86154326328750ae",
"service": "conductor",
"zone": "internal"
},
{
"host_name": "39f55087a1024d1380755951c945ca69",
"service": "cells",
"zone": "internal"
}
]
}

View File

@ -5,4 +5,5 @@
<host host_name="2d1bdd671b5d41fd89dec74be5770c63" service="network"/>
<host host_name="7c2dd5ecb7494dd1bf4240b7f7f9bf3a" service="scheduler"/>
<host host_name="f9c273d8e03141a2a01def0ad18e5be4" service="conductor"/>
</hosts>
<host host_name="2b893569cd824b979bd80a2c94570a1f" service="cells"/>
</hosts>

View File

@ -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",

View File

@ -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]

View File

@ -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)

View File

@ -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()

View File

@ -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')

View File

@ -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:
@ -266,6 +266,15 @@ class CellStateManager(base.Base):
self._refresh_cells_from_db(ctxt)
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."""

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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')

View File

@ -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": "",

View File

@ -88,6 +88,14 @@
"namespace": "http://docs.openstack.org/compute/ext/availabilityzone/api/v1.1",
"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",

View File

@ -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>

View File

@ -0,0 +1,9 @@
{
"cell": {
"name": "cell3",
"username": "username3",
"rpc_host": null,
"rpc_port": null,
"type": "child"
}
}

View File

@ -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"/>

View File

@ -0,0 +1,4 @@
{
"cells": []
}

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/>

View File

@ -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"
}
]
}

View File

@ -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>

View File

@ -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"
}
]
}

View File

@ -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>

View File

@ -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()

View File

@ -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'