Merge "Cells: Add cells API extension"
This commit is contained in:
commit
a88152a476
@ -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()
|
||||
|
@ -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.",
|
||||
|
@ -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>
|
||||
|
9
doc/api_samples/os-cells/cells-get-resp.json
Normal file
9
doc/api_samples/os-cells/cells-get-resp.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"cell": {
|
||||
"name": "cell3",
|
||||
"rpc_host": null,
|
||||
"rpc_port": null,
|
||||
"type": "child",
|
||||
"username": "username3"
|
||||
}
|
||||
}
|
2
doc/api_samples/os-cells/cells-get-resp.xml
Normal file
2
doc/api_samples/os-cells/cells-get-resp.xml
Normal 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"/>
|
3
doc/api_samples/os-cells/cells-list-empty-resp.json
Normal file
3
doc/api_samples/os-cells/cells-list-empty-resp.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"cells": []
|
||||
}
|
2
doc/api_samples/os-cells/cells-list-empty-resp.xml
Normal file
2
doc/api_samples/os-cells/cells-list-empty-resp.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/>
|
39
doc/api_samples/os-cells/cells-list-resp.json
Normal file
39
doc/api_samples/os-cells/cells-list-resp.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
8
doc/api_samples/os-cells/cells-list-resp.xml
Normal file
8
doc/api_samples/os-cells/cells-list-resp.xml
Normal 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>
|
@ -24,6 +24,11 @@
|
||||
"host_name": "6e48bfe1a3304b7b86154326328750ae",
|
||||
"service": "conductor",
|
||||
"zone": "internal"
|
||||
},
|
||||
{
|
||||
"host_name": "39f55087a1024d1380755951c945ca69",
|
||||
"service": "cells",
|
||||
"zone": "internal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
303
nova/api/openstack/compute/contrib/cells.py
Normal file
303
nova/api/openstack/compute/contrib/cells.py
Normal 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]
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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')
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
396
nova/tests/api/openstack/compute/contrib/test_cells.py
Normal file
396
nova/tests/api/openstack/compute/contrib/test_cells.py
Normal 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)
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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": "",
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"cell": {
|
||||
"name": "cell3",
|
||||
"username": "username3",
|
||||
"rpc_host": null,
|
||||
"rpc_port": null,
|
||||
"type": "child"
|
||||
}
|
||||
}
|
@ -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"/>
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"cells": []
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<cells xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/>
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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>
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user