summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--etc/nova/policy.json1
-rw-r--r--nova/api/openstack/compute/contrib/extended_availability_zone.py11
-rw-r--r--nova/api/openstack/compute/plugins/v3/cells.py347
-rw-r--r--nova/api/openstack/compute/plugins/v3/flavor_disabled.py91
-rw-r--r--nova/availability_zones.py40
-rw-r--r--nova/db/sqlalchemy/utils.py16
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_availability_zone.py1
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_extended_availability_zone.py51
-rw-r--r--nova/tests/api/openstack/compute/plugins/v3/test_cells.py474
-rw-r--r--nova/tests/api/openstack/compute/plugins/v3/test_flavor_disabled.py101
-rw-r--r--nova/tests/api/openstack/fakes.py5
-rw-r--r--nova/tests/db/test_migration_utils.py27
-rw-r--r--nova/tests/fake_policy.py1
-rw-r--r--nova/tests/virt/vmwareapi/test_vmwareapi.py2
-rw-r--r--nova/tests/virt/vmwareapi/test_vmwareapi_vm_util.py96
-rwxr-xr-xnova/virt/vmwareapi/driver.py22
-rw-r--r--nova/virt/vmwareapi/fake.py75
-rw-r--r--nova/virt/vmwareapi/vm_util.py106
-rw-r--r--nova/virt/vmwareapi/vmops.py20
-rw-r--r--setup.cfg1
20 files changed, 1457 insertions, 31 deletions
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index d37ee60a4..7c563b936 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -59,6 +59,7 @@
"compute_extension:flavor_access": "",
"compute_extension:v3:os-flavor-access": "",
"compute_extension:flavor_disabled": "",
+ "compute_extension:v3:os-flavor-disabled": "",
"compute_extension:flavor_rxtx": "",
"compute_extension:flavor_swap": "",
"compute_extension:flavorextradata": "",
diff --git a/nova/api/openstack/compute/contrib/extended_availability_zone.py b/nova/api/openstack/compute/contrib/extended_availability_zone.py
index 00765e209..6e77cd6bd 100644
--- a/nova/api/openstack/compute/contrib/extended_availability_zone.py
+++ b/nova/api/openstack/compute/contrib/extended_availability_zone.py
@@ -20,7 +20,7 @@
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
-from nova import availability_zones
+from nova import availability_zones as avail_zone
authorize = extensions.soft_extension_authorizer('compute',
'extended_availability_zone')
@@ -29,8 +29,13 @@ authorize = extensions.soft_extension_authorizer('compute',
class ExtendedAZController(wsgi.Controller):
def _extend_server(self, context, server, instance):
key = "%s:availability_zone" % Extended_availability_zone.alias
- server[key] = availability_zones.get_instance_availability_zone(
- context, instance)
+ az = avail_zone.get_instance_availability_zone(context, instance)
+ if not az and instance.get('availability_zone'):
+ # Likely hasn't reached a viable compute node yet so give back the
+ # desired availability_zone that *may* exist in the instance
+ # record itself.
+ az = instance['availability_zone']
+ server[key] = az
@wsgi.extends
def show(self, req, resp_obj, id):
diff --git a/nova/api/openstack/compute/plugins/v3/cells.py b/nova/api/openstack/compute/plugins/v3/cells.py
new file mode 100644
index 000000000..4e809d70e
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/cells.py
@@ -0,0 +1,347 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011-2012 OpenStack Foundation
+# 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 oslo.config import cfg
+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 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
+ make_capacity(elem)
+
+
+def make_capacity(cell):
+
+ def get_units_by_mb(capacity_info):
+ return capacity_info['units_by_mb'].items()
+
+ capacity = xmlutil.SubTemplateElement(cell, 'capacities',
+ selector='capacities')
+
+ ram_free = xmlutil.SubTemplateElement(capacity, 'ram_free',
+ selector='ram_free')
+ ram_free.set('total_mb', 'total_mb')
+ unit_by_mb = xmlutil.SubTemplateElement(ram_free, 'unit_by_mb',
+ selector=get_units_by_mb)
+ unit_by_mb.set('mb', 0)
+ unit_by_mb.set('unit', 1)
+
+ disk_free = xmlutil.SubTemplateElement(capacity, 'disk_free',
+ selector='disk_free')
+ disk_free.set('total_mb', 'total_mb')
+ unit_by_mb = xmlutil.SubTemplateElement(disk_free, 'unit_by_mb',
+ selector=get_units_by_mb)
+ unit_by_mb.set('mb', 0)
+ unit_by_mb.set('unit', 1)
+
+cell_nsmap = {None: wsgi.XMLNS_V10}
+
+
+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."""
+ node = xmlutil.safe_minidom_parse_string(string)
+
+ 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, ext_mgr):
+ self.compute_api = compute.API()
+ self.cells_rpcapi = cells_rpcapi.CellsAPI()
+ self.ext_mgr = ext_mgr
+
+ def _get_cells(self, ctxt, req, detail=False):
+ """Return all cells."""
+ # 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 capacities(self, req, id=None):
+ """Return capacities for a given cell or all cells."""
+ # TODO(kaushikc): return capacities as a part of cell info and
+ # cells detail calls in v3, along with capabilities
+ if not self.ext_mgr.is_loaded('os-cell-capacities'):
+ raise exc.HTTPNotFound()
+
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ capacities = self.cells_rpcapi.get_capacities(context,
+ cell_name=id)
+ except exception.CellNotFound:
+ msg = (_("Cell %(id)s not found.") % {'id': id})
+ raise exc.HTTPNotFound(explanation=msg)
+
+ return dict(cell={"capacities": capacities})
+
+ @wsgi.serializers(xml=CellTemplate)
+ def show(self, req, id):
+ """Return data about the given cell name. 'id' is a cell name."""
+ 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 = "2013-05-14T00:00:00+00:00"
+
+ def get_resources(self):
+ coll_actions = {
+ 'detail': 'GET',
+ 'info': 'GET',
+ 'sync_instances': 'POST',
+ 'capacities': 'GET',
+ }
+ memb_actions = {
+ 'capacities': 'GET',
+ }
+
+ res = extensions.ResourceExtension('os-cells',
+ Controller(self.ext_mgr), collection_actions=coll_actions,
+ member_actions=memb_actions)
+ return [res]
diff --git a/nova/api/openstack/compute/plugins/v3/flavor_disabled.py b/nova/api/openstack/compute/plugins/v3/flavor_disabled.py
new file mode 100644
index 000000000..79f6df9ce
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/flavor_disabled.py
@@ -0,0 +1,91 @@
+# Copyright 2012 Nebula, Inc.
+#
+# 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 Flavor Disabled API extension."""
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+
+ALIAS = 'os-flavor-disabled'
+authorize = extensions.soft_extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+class FlavorDisabledController(wsgi.Controller):
+ def _extend_flavors(self, req, flavors):
+ for flavor in flavors:
+ db_flavor = req.get_db_flavor(flavor['id'])
+ key = "%s:disabled" % FlavorDisabled.alias
+ flavor[key] = db_flavor['disabled']
+
+ def _show(self, req, resp_obj):
+ if not authorize(req.environ['nova.context']):
+ return
+ if 'flavor' in resp_obj.obj:
+ resp_obj.attach(xml=FlavorDisabledTemplate())
+ self._extend_flavors(req, [resp_obj.obj['flavor']])
+
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ return self._show(req, resp_obj)
+
+ @wsgi.extends(action='create')
+ def create(self, req, resp_obj, body):
+ return self._show(req, resp_obj)
+
+ @wsgi.extends
+ def detail(self, req, resp_obj):
+ if not authorize(req.environ['nova.context']):
+ return
+ resp_obj.attach(xml=FlavorsDisabledTemplate())
+ self._extend_flavors(req, list(resp_obj.obj['flavors']))
+
+
+class FlavorDisabled(extensions.V3APIExtensionBase):
+ """Support to show the disabled status of a flavor."""
+
+ name = "FlavorDisabled"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS
+ version = 1
+
+ def get_controller_extensions(self):
+ controller = FlavorDisabledController()
+ extension = extensions.ControllerExtension(self, 'flavors', controller)
+ return [extension]
+
+ def get_resources(self):
+ return []
+
+
+def make_flavor(elem):
+ elem.set('{%s}disabled' % FlavorDisabled.namespace,
+ '%s:disabled' % FlavorDisabled.alias)
+
+
+class FlavorDisabledTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor', selector='flavor')
+ make_flavor(root)
+ return xmlutil.SlaveTemplate(root, 1, nsmap={
+ FlavorDisabled.alias: FlavorDisabled.namespace})
+
+
+class FlavorsDisabledTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem)
+ return xmlutil.SlaveTemplate(root, 1, nsmap={
+ FlavorDisabled.alias: FlavorDisabled.namespace})
diff --git a/nova/availability_zones.py b/nova/availability_zones.py
index 6d111c120..216c46041 100644
--- a/nova/availability_zones.py
+++ b/nova/availability_zones.py
@@ -23,7 +23,7 @@ from nova.openstack.common import memorycache
# NOTE(vish): azs don't change that often, so cache them for an hour to
# avoid hitting the db multiple times on every request.
AZ_CACHE_SECONDS = 60 * 60
-MC = memorycache.get_client()
+MC = None
availability_zone_opts = [
cfg.StrOpt('internal_service_availability_zone',
@@ -38,6 +38,27 @@ CONF = cfg.CONF
CONF.register_opts(availability_zone_opts)
+def _get_cache():
+ global MC
+
+ if MC is None:
+ MC = memorycache.get_client()
+
+ return MC
+
+
+def _reset_cache():
+ """Reset the cache, mainly for testing purposes."""
+
+ global MC
+
+ MC = None
+
+
+def _make_cache_key(host):
+ return "azcache-%s" % host
+
+
def set_availability_zones(context, services):
# Makes sure services isn't a sqlalchemy object
services = [dict(service.iteritems()) for service in services]
@@ -50,6 +71,11 @@ def set_availability_zones(context, services):
az = u','.join(list(metadata[service['host']]))
else:
az = CONF.default_availability_zone
+ # update the cache
+ cache = _get_cache()
+ cache_key = _make_cache_key(service['host'])
+ cache.delete(cache_key)
+ cache.set(cache_key, az, AZ_CACHE_SECONDS)
service['availability_zone'] = az
return services
@@ -62,9 +88,10 @@ def get_host_availability_zone(context, host, conductor_api=None):
metadata = db.aggregate_metadata_get_by_host(
context, host, key='availability_zone')
if 'availability_zone' in metadata:
- return list(metadata['availability_zone'])[0]
+ az = list(metadata['availability_zone'])[0]
else:
- return CONF.default_availability_zone
+ az = CONF.default_availability_zone
+ return az
def get_availability_zones(context):
@@ -95,10 +122,11 @@ def get_instance_availability_zone(context, instance):
if not host:
return None
- cache_key = "azcache-%s" % host
- az = MC.get(cache_key)
+ cache_key = _make_cache_key(host)
+ cache = _get_cache()
+ az = cache.get(cache_key)
if not az:
elevated = context.elevated()
az = get_host_availability_zone(elevated, host)
- MC.set(cache_key, az, AZ_CACHE_SECONDS)
+ cache.set(cache_key, az, AZ_CACHE_SECONDS)
return az
diff --git a/nova/db/sqlalchemy/utils.py b/nova/db/sqlalchemy/utils.py
index 6c0a53846..918f50bfd 100644
--- a/nova/db/sqlalchemy/utils.py
+++ b/nova/db/sqlalchemy/utils.py
@@ -17,7 +17,7 @@
import re
-from migrate.changeset import UniqueConstraint
+from migrate.changeset import UniqueConstraint, ForeignKeyConstraint
from sqlalchemy import Boolean
from sqlalchemy import CheckConstraint
from sqlalchemy import Column
@@ -101,7 +101,8 @@ def _get_unique_constraints_in_sqlite(migrate_engine, table_name):
uniques = set([
schema.UniqueConstraint(
- *[getattr(table.c, c.strip()) for c in cols.split(",")], name=name
+ *[getattr(table.c, c.strip(' "'))
+ for c in cols.split(",")], name=name
)
for name, cols in re.findall(regexp, sql_data)
])
@@ -128,7 +129,8 @@ def _drop_unique_constraint_in_sqlite(migrate_engine, table_name, uc_name,
table.constraints.update(uniques)
constraints = [constraint for constraint in table.constraints
- if not constraint.name == uc_name]
+ if not constraint.name == uc_name and
+ not isinstance(constraint, schema.ForeignKeyConstraint)]
new_table = Table(table_name + "__tmp__", meta, *(columns + constraints))
new_table.create()
@@ -139,12 +141,20 @@ def _drop_unique_constraint_in_sqlite(migrate_engine, table_name, uc_name,
indexes.append(Index(index["name"],
*column_names,
unique=index["unique"]))
+ f_keys = []
+ for fk in insp.get_foreign_keys(table_name):
+ refcolumns = [fk['referred_table'] + '.' + col
+ for col in fk['referred_columns']]
+ f_keys.append(ForeignKeyConstraint(fk['constrained_columns'],
+ refcolumns, table=new_table, name=fk['name']))
ins = InsertFromSelect(new_table, table.select())
migrate_engine.execute(ins)
table.drop()
[index.create(migrate_engine) for index in indexes]
+ for fkey in f_keys:
+ fkey.create()
new_table.rename(table_name)
diff --git a/nova/tests/api/openstack/compute/contrib/test_availability_zone.py b/nova/tests/api/openstack/compute/contrib/test_availability_zone.py
index 2ccb9fa31..0b63960ce 100644
--- a/nova/tests/api/openstack/compute/contrib/test_availability_zone.py
+++ b/nova/tests/api/openstack/compute/contrib/test_availability_zone.py
@@ -76,6 +76,7 @@ def fake_set_availability_zones(context, services):
class AvailabilityZoneApiTest(test.TestCase):
def setUp(self):
super(AvailabilityZoneApiTest, self).setUp()
+ availability_zones._reset_cache()
self.stubs.Set(db, 'service_get_all', fake_service_get_all)
self.stubs.Set(availability_zones, 'set_availability_zones',
fake_set_availability_zones)
diff --git a/nova/tests/api/openstack/compute/contrib/test_extended_availability_zone.py b/nova/tests/api/openstack/compute/contrib/test_extended_availability_zone.py
index 814c0fff4..59d60acf2 100644
--- a/nova/tests/api/openstack/compute/contrib/test_extended_availability_zone.py
+++ b/nova/tests/api/openstack/compute/contrib/test_extended_availability_zone.py
@@ -19,6 +19,7 @@ import webob
from nova.api.openstack.compute.contrib import extended_availability_zone
from nova import availability_zones
from nova import compute
+from nova.compute import vm_states
from nova import exception
from nova.openstack.common import jsonutils
from nova import test
@@ -29,14 +30,31 @@ UUID2 = '00000000-0000-0000-0000-000000000002'
UUID3 = '00000000-0000-0000-0000-000000000003'
+def fake_compute_get_az(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3, host="get-host",
+ vm_state=vm_states.ACTIVE,
+ availability_zone='fakeaz')
+ return inst
+
+
+def fake_compute_get_empty(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3, host="",
+ vm_state=vm_states.ACTIVE,
+ availability_zone='fakeaz')
+ return inst
+
+
def fake_compute_get(*args, **kwargs):
- inst = fakes.stub_instance(1, uuid=UUID3, host="get-host")
+ inst = fakes.stub_instance(1, uuid=UUID3, host="get-host",
+ vm_state=vm_states.ACTIVE)
return inst
def fake_compute_get_all(*args, **kwargs):
- inst1 = fakes.stub_instance(1, uuid=UUID1, host="all-host")
- inst2 = fakes.stub_instance(2, uuid=UUID2, host="all-host")
+ inst1 = fakes.stub_instance(1, uuid=UUID1, host="all-host",
+ vm_state=vm_states.ACTIVE)
+ inst2 = fakes.stub_instance(2, uuid=UUID2, host="all-host",
+ vm_state=vm_states.ACTIVE)
return [inst1, inst2]
@@ -44,12 +62,17 @@ def fake_get_host_availability_zone(context, host):
return host
+def fake_get_no_host_availability_zone(context, host):
+ return None
+
+
class ExtendedServerAttributesTest(test.TestCase):
content_type = 'application/json'
prefix = 'OS-EXT-AZ:'
def setUp(self):
super(ExtendedServerAttributesTest, self).setUp()
+ availability_zones._reset_cache()
fakes.stub_out_nw_api(self.stubs)
self.stubs.Set(compute.api.API, 'get', fake_compute_get)
self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
@@ -77,6 +100,28 @@ class ExtendedServerAttributesTest(test.TestCase):
self.assertEqual(server.get('%savailability_zone' % self.prefix),
az)
+ def test_show_no_host_az(self):
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get_az)
+ self.stubs.Set(availability_zones, 'get_host_availability_zone',
+ fake_get_no_host_availability_zone)
+
+ url = '/v2/fake/servers/%s' % UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertServerAttributes(self._get_server(res.body), 'fakeaz')
+
+ def test_show_empty_host_az(self):
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get_empty)
+ self.stubs.Set(availability_zones, 'get_host_availability_zone',
+ fake_get_no_host_availability_zone)
+
+ url = '/v2/fake/servers/%s' % UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertServerAttributes(self._get_server(res.body), 'fakeaz')
+
def test_show(self):
url = '/v2/fake/servers/%s' % UUID3
res = self._make_request(url)
diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_cells.py b/nova/tests/api/openstack/compute/plugins/v3/test_cells.py
new file mode 100644
index 000000000..a9e77693e
--- /dev/null
+++ b/nova/tests/api/openstack/compute/plugins/v3/test_cells.py
@@ -0,0 +1,474 @@
+# Copyright 2011-2012 OpenStack Foundation
+# 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 extensions
+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
+from nova.tests import utils
+
+
+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.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.controller = cells_ext.Controller(self.ext_mgr)
+ self.context = context.get_admin_context()
+
+ def _get_request(self, resource):
+ 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_show_capacities(self):
+ self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True)
+ self.mox.StubOutWithMock(self.controller.cells_rpcapi,
+ 'get_capacities')
+ response = {"ram_free":
+ {"units_by_mb": {"8192": 0, "512": 13,
+ "4096": 1, "2048": 3, "16384": 0},
+ "total_mb": 7680},
+ "disk_free":
+ {"units_by_mb": {"81920": 11, "20480": 46,
+ "40960": 23, "163840": 5, "0": 0},
+ "total_mb": 1052672}
+ }
+ self.controller.cells_rpcapi.\
+ get_capacities(self.context, cell_name=None).AndReturn(response)
+ self.mox.ReplayAll()
+ req = self._get_request("cells/capacities")
+ req.environ["nova.context"] = self.context
+ res_dict = self.controller.capacities(req)
+ self.assertEqual(response, res_dict['cell']['capacities'])
+
+ def test_show_capacity_fails_with_non_admin_context(self):
+ self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True)
+ rules = {"compute_extension:cells": "is_admin:true"}
+ self.policy.set_rules(rules)
+
+ self.mox.ReplayAll()
+ req = self._get_request("cells/capacities")
+ req.environ["nova.context"] = self.context
+ req.environ["nova.context"].is_admin = False
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.capacities, req)
+
+ def test_show_capacities_for_invalid_cell(self):
+ self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True)
+ self.mox.StubOutWithMock(self.controller.cells_rpcapi,
+ 'get_capacities')
+ self.controller.cells_rpcapi. \
+ get_capacities(self.context, cell_name="invalid_cell").AndRaise(
+ exception.CellNotFound(cell_name="invalid_cell"))
+ self.mox.ReplayAll()
+ req = self._get_request("cells/invalid_cell/capacities")
+ req.environ["nova.context"] = self.context
+ self.assertRaises(exc.HTTPNotFound,
+ self.controller.capacities, req, "invalid_cell")
+
+ def test_show_capacities_for_cell(self):
+ self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True)
+ self.mox.StubOutWithMock(self.controller.cells_rpcapi,
+ 'get_capacities')
+ response = {"ram_free":
+ {"units_by_mb": {"8192": 0, "512": 13,
+ "4096": 1, "2048": 3, "16384": 0},
+ "total_mb": 7680},
+ "disk_free":
+ {"units_by_mb": {"81920": 11, "20480": 46,
+ "40960": 23, "163840": 5, "0": 0},
+ "total_mb": 1052672}
+ }
+ self.controller.cells_rpcapi.\
+ get_capacities(self.context, cell_name='cell_name').\
+ AndReturn(response)
+ self.mox.ReplayAll()
+ req = self._get_request("cells/capacities")
+ req.environ["nova.context"] = self.context
+ res_dict = self.controller.capacities(req, 'cell_name')
+ self.assertEqual(response, res_dict['cell']['capacities'])
+
+ def test_sync_instances(self):
+ call_info = {}
+
+ 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)
+
+ def test_with_corrupt_xml(self):
+ deserializer = cells_ext.CellDeserializer()
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ deserializer.deserialize,
+ utils.killer_xml_body())
diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_flavor_disabled.py b/nova/tests/api/openstack/compute/plugins/v3/test_flavor_disabled.py
new file mode 100644
index 000000000..5ff7f4035
--- /dev/null
+++ b/nova/tests/api/openstack/compute/plugins/v3/test_flavor_disabled.py
@@ -0,0 +1,101 @@
+# Copyright 2012 Nebula, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+import webob
+
+from nova.api.openstack.compute.plugins.v3 import flavor_disabled
+from nova.compute import flavors
+from nova.openstack.common import jsonutils
+from nova import test
+from nova.tests.api.openstack import fakes
+
+FAKE_FLAVORS = {
+ 'flavor 1': {
+ "flavorid": '1',
+ "name": 'flavor 1',
+ "memory_mb": '256',
+ "root_gb": '10',
+ "disabled": False,
+ },
+ 'flavor 2': {
+ "flavorid": '2',
+ "name": 'flavor 2',
+ "memory_mb": '512',
+ "root_gb": '20',
+ "disabled": True,
+ },
+}
+
+
+def fake_flavor_get_by_flavor_id(flavorid):
+ return FAKE_FLAVORS['flavor %s' % flavorid]
+
+
+def fake_flavor_get_all(*args, **kwargs):
+ return FAKE_FLAVORS
+
+
+class FlavorDisabledTest(test.TestCase):
+ content_type = 'application/json'
+ prefix = '%s:' % flavor_disabled.FlavorDisabled.alias
+
+ def setUp(self):
+ super(FlavorDisabledTest, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(flavors, "get_all_flavors",
+ fake_flavor_get_all)
+ self.stubs.Set(flavors,
+ "get_flavor_by_flavor_id",
+ fake_flavor_get_by_flavor_id)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ app = fakes.wsgi_app_v3(init_only=('servers', 'flavors',
+ 'os-flavor-disabled'))
+ return req.get_response(app)
+
+ def _get_flavor(self, body):
+ return jsonutils.loads(body).get('flavor')
+
+ def _get_flavors(self, body):
+ return jsonutils.loads(body).get('flavors')
+
+ def assertFlavorDisabled(self, flavor, disabled):
+ self.assertEqual(str(flavor.get('%sdisabled' % self.prefix)), disabled)
+
+ def test_show(self):
+ res = self._make_request('/v3/flavors/1')
+ self.assertEqual(res.status_int, 200, res.body)
+ self.assertFlavorDisabled(self._get_flavor(res.body), 'False')
+
+ def test_detail(self):
+ res = self._make_request('/v3/flavors/detail')
+
+ self.assertEqual(res.status_int, 200, res.body)
+ flavors = self._get_flavors(res.body)
+ self.assertFlavorDisabled(flavors[0], 'False')
+ self.assertFlavorDisabled(flavors[1], 'True')
+
+
+class FlavorDisabledXmlTest(FlavorDisabledTest):
+ content_type = 'application/xml'
+ prefix = '{%s}' % flavor_disabled.FlavorDisabled.namespace
+
+ def _get_flavor(self, body):
+ return etree.XML(body)
+
+ def _get_flavors(self, body):
+ return etree.XML(body).getchildren()
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 6c30b9a9e..d0239885d 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -464,7 +464,8 @@ def stub_instance(id, user_id=None, project_id=None, host=None,
security_groups=None, root_device_name=None,
limit=None, marker=None,
launched_at=datetime.datetime.utcnow(),
- terminated_at=datetime.datetime.utcnow()):
+ terminated_at=datetime.datetime.utcnow(),
+ availability_zone=''):
if user_id is None:
user_id = 'fake_user'
@@ -535,7 +536,7 @@ def stub_instance(id, user_id=None, project_id=None, host=None,
"scheduled_at": timeutils.utcnow(),
"launched_at": launched_at,
"terminated_at": terminated_at,
- "availability_zone": "",
+ "availability_zone": availability_zone,
"display_name": display_name or server_name,
"display_description": "",
"locked": False,
diff --git a/nova/tests/db/test_migration_utils.py b/nova/tests/db/test_migration_utils.py
index 3674a7c45..af206632d 100644
--- a/nova/tests/db/test_migration_utils.py
+++ b/nova/tests/db/test_migration_utils.py
@@ -19,7 +19,7 @@ import warnings
from migrate.changeset import UniqueConstraint
from sqlalchemy.dialects import mysql
from sqlalchemy import Boolean, Index, Integer, DateTime, String
-from sqlalchemy import MetaData, Table, Column
+from sqlalchemy import MetaData, Table, Column, ForeignKey
from sqlalchemy.engine import reflection
from sqlalchemy.exc import NoSuchTableError
from sqlalchemy.exc import SAWarning
@@ -514,3 +514,28 @@ class TestMigrationUtils(test_migrations.BaseMigrationTestCase):
# but sqlalchemy will set it to NullType.
self.assertTrue(isinstance(table.c.foo.type, NullType))
self.assertTrue(isinstance(table.c.deleted.type, Boolean))
+
+ def test_drop_unique_constraint_in_sqlite_fk_recreate(self):
+ engine = self.engines['sqlite']
+ meta = MetaData()
+ meta.bind = engine
+ parent_table = Table('table0', meta,
+ Column('id', Integer, primary_key=True),
+ Column('foo', Integer))
+ parent_table.create()
+ table_name = 'table1'
+ table = Table(table_name, meta,
+ Column('id', Integer, primary_key=True),
+ Column('baz', Integer),
+ Column('bar', Integer, ForeignKey("table0.id")),
+ UniqueConstraint('baz', name='constr1'))
+ table.create()
+ utils.drop_unique_constraint(engine, table_name, 'constr1', 'baz')
+
+ insp = reflection.Inspector.from_engine(engine)
+ f_keys = insp.get_foreign_keys(table_name)
+ self.assertEqual(len(f_keys), 1)
+ f_key = f_keys[0]
+ self.assertEqual(f_key['referred_table'], 'table0')
+ self.assertEqual(f_key['referred_columns'], ['id'])
+ self.assertEqual(f_key['constrained_columns'], ['bar'])
diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py
index bbde1787a..da43115d0 100644
--- a/nova/tests/fake_policy.py
+++ b/nova/tests/fake_policy.py
@@ -135,6 +135,7 @@ policy_data = """
"compute_extension:flavor_access": "",
"compute_extension:v3:os-flavor-access": "",
"compute_extension:flavor_disabled": "",
+ "compute_extension:v3:os-flavor-disabled": "",
"compute_extension:flavor_rxtx": "",
"compute_extension:flavor_swap": "",
"compute_extension:flavorextradata": "",
diff --git a/nova/tests/virt/vmwareapi/test_vmwareapi.py b/nova/tests/virt/vmwareapi/test_vmwareapi.py
index 62db10535..afda26b0d 100644
--- a/nova/tests/virt/vmwareapi/test_vmwareapi.py
+++ b/nova/tests/virt/vmwareapi/test_vmwareapi.py
@@ -531,7 +531,7 @@ class VMwareAPIVMTestCase(test.TestCase):
self._create_instance_in_the_db()
self._create_vm()
vnc_dict = self.conn.get_vnc_console(self.instance)
- self.assertEquals(vnc_dict['host'], "test_url")
+ self.assertEquals(vnc_dict['host'], "ha-host")
self.assertEquals(vnc_dict['port'], 5910)
def test_host_ip_addr(self):
diff --git a/nova/tests/virt/vmwareapi/test_vmwareapi_vm_util.py b/nova/tests/virt/vmwareapi/test_vmwareapi_vm_util.py
index 123a314c1..0456dfece 100644
--- a/nova/tests/virt/vmwareapi/test_vmwareapi_vm_util.py
+++ b/nova/tests/virt/vmwareapi/test_vmwareapi_vm_util.py
@@ -16,6 +16,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from collections import namedtuple
+
from nova import exception
from nova import test
from nova.virt.vmwareapi import fake
@@ -33,9 +35,11 @@ class fake_session(object):
class VMwareVMUtilTestCase(test.TestCase):
def setUp(self):
super(VMwareVMUtilTestCase, self).setUp()
+ fake.reset()
def tearDown(self):
super(VMwareVMUtilTestCase, self).tearDown()
+ fake.reset()
def test_get_datastore_ref_and_name(self):
result = vm_util.get_datastore_ref_and_name(
@@ -54,3 +58,95 @@ class VMwareVMUtilTestCase(test.TestCase):
self.assertRaises(exception.DatastoreNotFound,
vm_util.get_datastore_ref_and_name,
fake_session(), cluster="fake-cluster")
+
+ def test_get_host_ref_from_id(self):
+
+ fake_host_sys = fake.HostSystem(
+ fake.ManagedObjectReference("HostSystem", "host-123"))
+
+ fake_host_id = fake_host_sys.obj.value
+ fake_host_name = "ha-host"
+
+ ref = vm_util.get_host_ref_from_id(
+ fake_session([fake_host_sys]), fake_host_id, ['name'])
+
+ self.assertIsInstance(ref, fake.HostSystem)
+ self.assertEqual(fake_host_id, ref.obj.value)
+
+ host_name = vm_util.get_host_name_from_host_ref(ref)
+
+ self.assertEquals(fake_host_name, host_name)
+
+ def test_get_host_name_for_vm(self):
+
+ fake_vm = fake.ManagedObject(
+ "VirtualMachine", fake.ManagedObjectReference(
+ "vm-123", "VirtualMachine"))
+ fake_vm.propSet.append(
+ fake.Property('name', 'vm-123'))
+
+ vm_ref = vm_util.get_vm_ref_from_name(
+ fake_session([fake_vm]), 'vm-123')
+
+ self.assertIsNotNone(vm_ref)
+
+ fake_results = [
+ fake.ObjectContent(
+ None, [
+ fake.Property('runtime.host',
+ fake.ManagedObjectReference(
+ 'host-123', 'HostSystem'))
+ ])]
+
+ host_id = vm_util.get_host_id_from_vm_ref(
+ fake_session(fake_results), vm_ref)
+
+ self.assertEqual('host-123', host_id)
+
+ def test_property_from_property_set(self):
+
+ ObjectContent = namedtuple('ObjectContent', ['propSet'])
+ DynamicProperty = namedtuple('Property', ['name', 'val'])
+ MoRef = namedtuple('Val', ['value'])
+
+ results_good = [
+ ObjectContent(propSet=[
+ DynamicProperty(name='name', val=MoRef(value='vm-123'))]),
+ ObjectContent(propSet=[
+ DynamicProperty(name='foo', val=MoRef(value='bar1')),
+ DynamicProperty(
+ name='runtime.host', val=MoRef(value='host-123')),
+ DynamicProperty(name='foo', val=MoRef(value='bar2')),
+ ]),
+ ObjectContent(propSet=[
+ DynamicProperty(
+ name='something', val=MoRef(value='thing'))]), ]
+
+ results_bad = [
+ ObjectContent(propSet=[
+ DynamicProperty(name='name', val=MoRef(value='vm-123'))]),
+ ObjectContent(propSet=[
+ DynamicProperty(name='foo', val='bar1'),
+ DynamicProperty(name='foo', val='bar2'), ]),
+ ObjectContent(propSet=[
+ DynamicProperty(
+ name='something', val=MoRef(value='thing'))]), ]
+
+ prop = vm_util.property_from_property_set(
+ 'runtime.host', results_good)
+ self.assertIsNotNone(prop)
+ value = prop.val.value
+ self.assertEqual('host-123', value)
+
+ prop2 = vm_util.property_from_property_set(
+ 'runtime.host', results_bad)
+ self.assertIsNone(prop2)
+
+ prop3 = vm_util.property_from_property_set('foo', results_good)
+ self.assertIsNotNone(prop3)
+ val3 = prop3.val.value
+ self.assertEqual('bar1', val3)
+
+ prop4 = vm_util.property_from_property_set('foo', results_bad)
+ self.assertIsNotNone(prop4)
+ self.assertEqual('bar1', prop4.val)
diff --git a/nova/virt/vmwareapi/driver.py b/nova/virt/vmwareapi/driver.py
index 1f91e5ab2..8389b2c3d 100755
--- a/nova/virt/vmwareapi/driver.py
+++ b/nova/virt/vmwareapi/driver.py
@@ -127,6 +127,12 @@ class Failure(Exception):
class VMwareESXDriver(driver.ComputeDriver):
"""The ESX host connection object."""
+ # VMwareAPI has both ESXi and vCenter API sets.
+ # The ESXi API are a proper sub-set of the vCenter API.
+ # That is to say, nearly all valid ESXi calls are
+ # valid vCenter calls. There are some small edge-case
+ # exceptions regarding VNC, CIM, User management & SSO.
+
def __init__(self, virtapi, read_only=False, scheme="https"):
super(VMwareESXDriver, self).__init__(virtapi)
@@ -338,6 +344,14 @@ class VMwareESXDriver(driver.ComputeDriver):
class VMwareVCDriver(VMwareESXDriver):
"""The ESX host connection object."""
+ # The vCenter driver includes several additional VMware vSphere
+ # capabilities that include API that act on hosts or groups of
+ # hosts in clusters or non-cluster logical-groupings.
+ #
+ # vCenter is not a hypervisor itself, it works with multiple
+ # hypervisor host machines and their guests. This fact can
+ # subtly alter how vSphere and OpenStack interoperate.
+
def __init__(self, virtapi, read_only=False, scheme="https"):
super(VMwareVCDriver, self).__init__(virtapi)
self._cluster_name = CONF.vmwareapi_cluster_name
@@ -399,6 +413,14 @@ class VMwareVCDriver(VMwareESXDriver):
post_method, recover_method,
block_migration)
+ def get_vnc_console(self, instance):
+ """Return link to instance's VNC console using vCenter logic."""
+ # In this situation, ESXi and vCenter require different
+ # API logic to create a valid VNC console connection object.
+ # In specific, vCenter does not actually run the VNC service
+ # itself. You must talk to the VNC host underneath vCenter.
+ return self._vmops.get_vnc_console_vcenter(instance)
+
class VMwareAPISession(object):
"""
diff --git a/nova/virt/vmwareapi/fake.py b/nova/virt/vmwareapi/fake.py
index cd8302115..abf896c79 100644
--- a/nova/virt/vmwareapi/fake.py
+++ b/nova/virt/vmwareapi/fake.py
@@ -21,6 +21,7 @@
A fake VMware VI API implementation.
"""
+import collections
import pprint
import uuid
@@ -83,28 +84,72 @@ def _get_objects(obj_type):
return lst_objs
-class Prop(object):
+class Property(object):
"""Property Object base class."""
- def __init__(self):
- self.name = None
- self.val = None
+ def __init__(self, name=None, val=None):
+ self.name = name
+ self.val = val
+
+class ManagedObjectReference(object):
+ """A managed object reference is a remote identifier."""
-class Obj(object):
- def __init__(self, name, value):
+ def __init__(self, value="object-123", _type="ManagedObject"):
+ super(ManagedObjectReference, self)
+ # Managed Object Reference value attributes
+ # typically have values like vm-123 or
+ # host-232 and not UUID.
self.value = value
- self._type = name
+ # Managed Object Reference _type
+ # attributes hold the name of the type
+ # of the vCenter object the value
+ # attribute is the identifier for
+ self._type = _type
+
+
+class ObjectContent(object):
+ """ObjectContent array holds dynamic properties."""
+
+ # This class is a *fake* of a class sent back to us by
+ # SOAP. It has its own names. These names are decided
+ # for us by the API we are *faking* here.
+ def __init__(self, obj_ref, prop_list=None, missing_list=None):
+ self.obj = obj_ref
+
+ if not isinstance(prop_list, collections.Iterable):
+ prop_list = []
+
+ if not isinstance(missing_list, collections.Iterable):
+ missing_list = []
+
+ # propSet is the name your Python code will need to
+ # use since this is the name that the API will use
+ self.propSet = prop_list
+
+ # missingSet is the name your python code will
+ # need to use since this is the name that the
+ # API we are talking to will use.
+ self.missingSet = missing_list
class ManagedObject(object):
- """Managed Data Object base class."""
+ """Managed Object base class."""
def __init__(self, name="ManagedObject", obj_ref=None, value=None):
"""Sets the obj property which acts as a reference to the object."""
super(ManagedObject, self).__setattr__('objName', name)
+
+ # A managed object is a local representation of a
+ # remote object that you can reference using the
+ # object reference.
if obj_ref is None:
- obj_ref = Obj(name, value)
+ if value is None:
+ value = 'obj-123'
+ obj_ref = ManagedObjectReference(value, name)
+
+ # we use __setattr__ here because below the
+ # default setter has been altered for this class.
object.__setattr__(self, 'obj', obj_ref)
object.__setattr__(self, 'propSet', [])
@@ -124,16 +169,20 @@ class ManagedObject(object):
return self.__getattr__(attr)
def __setattr__(self, attr, val):
+ # TODO(hartsocks): this is adds unnecessary complexity to the class
for prop in self.propSet:
if prop.name == attr:
prop.val = val
return
- elem = Prop()
+ elem = Property()
elem.name = attr
elem.val = val
self.propSet.append(elem)
def __getattr__(self, attr):
+ # TODO(hartsocks): remove this
+ # in a real ManagedObject you have to iterate the propSet
+ # in a real ManagedObject, the propSet is a *set* not a list
for elem in self.propSet:
if elem.name == attr:
return elem.val
@@ -215,6 +264,8 @@ class VirtualMachine(ManagedObject):
self.set("summary.config.memorySizeMB", kwargs.get("mem", 1))
self.set("config.hardware.device", kwargs.get("virtual_device", None))
self.set("config.extraConfig", kwargs.get("extra_config", None))
+ self.set('runtime.host',
+ ManagedObjectReference(value='host-123', _type="HostSystem"))
self.device = kwargs.get("virtual_device")
def reconfig(self, factory, val):
@@ -310,8 +361,8 @@ class HostNetworkSystem(ManagedObject):
class HostSystem(ManagedObject):
"""Host System class."""
- def __init__(self):
- super(HostSystem, self).__init__("HostSystem")
+ def __init__(self, obj_ref=None, value='host-123'):
+ super(HostSystem, self).__init__("HostSystem", obj_ref, value)
self.set("name", "ha-host")
if _db_content.get("HostNetworkSystem", None) is None:
create_host_network_system()
diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py
index 117669700..d8e063cad 100644
--- a/nova/virt/vmwareapi/vm_util.py
+++ b/nova/virt/vmwareapi/vm_util.py
@@ -20,6 +20,7 @@ The VMware API VM utility module to build SOAP object specs.
"""
import copy
+
from nova import exception
from nova.virt.vmwareapi import vim_util
@@ -526,6 +527,111 @@ def get_vm_ref(session, instance):
return vm_ref
+def get_host_ref_from_id(session, host_id, property_list=None):
+ """Get a host reference object for a host_id string."""
+
+ if property_list is None:
+ property_list = ['name']
+
+ host_refs = session._call_method(
+ vim_util, "get_objects",
+ "HostSystem", property_list)
+
+ for ref in host_refs:
+ if ref.obj.value == host_id:
+ return ref
+
+
+def get_host_id_from_vm_ref(session, vm_ref):
+ """
+ This method allows you to find the managed object
+ ID of the host running a VM. Since vMotion can
+ change the value, you should not presume that this
+ is a value that you can cache for very long and
+ should be prepared to allow for it to change.
+
+ :param session: a vSphere API connection
+ :param vm_ref: a reference object to the running VM
+ :return: the host_id running the virtual machine
+ """
+
+ # to prevent typographical errors below
+ property_name = 'runtime.host'
+
+ # a property collector in VMware vSphere Management API
+ # is a set of local representations of remote values.
+ # property_set here, is a local representation of the
+ # properties we are querying for.
+ property_set = session._call_method(
+ vim_util, "get_object_properties",
+ None, vm_ref, vm_ref._type, [property_name])
+
+ prop = property_from_property_set(
+ property_name, property_set)
+
+ if prop is not None:
+ prop = prop.val.value
+ else:
+ # reaching here represents an impossible state
+ raise RuntimeError(
+ "Virtual Machine %s exists without a runtime.host!"
+ % (vm_ref))
+
+ return prop
+
+
+def property_from_property_set(property_name, property_set):
+ '''
+ Use this method to filter property collector results.
+
+ Because network traffic is expensive, multiple
+ VMwareAPI calls will sometimes pile-up properties
+ to be collected. That means results may contain
+ many different values for multiple purposes.
+
+ This helper will filter a list for a single result
+ and filter the properties of that result to find
+ the single value of whatever type resides in that
+ result. This could be a ManagedObjectReference ID
+ or a complex value.
+
+ :param property_name: name of property you want
+ :param property_set: all results from query
+ :return: the value of the property.
+ '''
+
+ for prop in property_set:
+ p = _property_from_propSet(prop.propSet, property_name)
+ if p is not None:
+ return p
+
+
+def _property_from_propSet(propSet, name='name'):
+ for p in propSet:
+ if p.name == name:
+ return p
+
+
+def get_host_ref_for_vm(session, instance, props):
+ """Get the ESXi host running a VM by its name."""
+
+ vm_ref = get_vm_ref(session, instance)
+ host_id = get_host_id_from_vm_ref(session, vm_ref)
+ return get_host_ref_from_id(session, host_id, props)
+
+
+def get_host_name_for_vm(session, instance):
+ """Get the ESXi host running a VM by its name."""
+ host_ref = get_host_ref_for_vm(session, instance, ['name'])
+ return get_host_name_from_host_ref(host_ref)
+
+
+def get_host_name_from_host_ref(host_ref):
+ p = _property_from_propSet(host_ref.propSet)
+ if p is not None:
+ return p.val
+
+
def get_cluster_ref_from_name(session, cluster_name):
"""Get reference to the cluster with the name specified."""
cls = session._call_method(vim_util, "get_objects",
diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py
index 1afead6b9..e8f63f1d7 100644
--- a/nova/virt/vmwareapi/vmops.py
+++ b/nova/virt/vmwareapi/vmops.py
@@ -1089,6 +1089,26 @@ class VMwareVMOps(object):
'port': self._get_vnc_port(vm_ref),
'internal_access_path': None}
+ def get_vnc_console_vcenter(self, instance):
+ """Return connection info for a vnc console using vCenter logic."""
+
+ # vCenter does not run virtual machines and does not run
+ # a VNC proxy. Instead, you need to tell OpenStack to talk
+ # directly to the ESX host running the VM you are attempting
+ # to connect to via VNC.
+
+ vnc_console = self.get_vnc_console(instance)
+ host_name = vm_util.get_host_name_for_vm(
+ self._session,
+ instance)
+ vnc_console['host'] = host_name
+
+ # NOTE: VM can move hosts in some situations. Debug for admins.
+ LOG.debug(_("VM %(uuid)s is currently on host %(host_name)s"),
+ {'uuid': instance['name'], 'host_name': host_name})
+
+ return vnc_console
+
@staticmethod
def _get_vnc_port(vm_ref):
"""Return VNC port for an VM."""
diff --git a/setup.cfg b/setup.cfg
index 5b52a5464..3d90a8a44 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -61,6 +61,7 @@ nova.api.v3.extensions =
fixed_ips = nova.api.openstack.compute.plugins.v3.fixed_ips:FixedIPs
flavors = nova.api.openstack.compute.plugins.v3.flavors:Flavors
flavor_access = nova.api.openstack.compute.plugins.v3.flavor_access:FlavorAccess
+ flavor_disabled = nova.api.openstack.compute.plugins.v3.flavor_disabled:FlavorDisabled
ips = nova.api.openstack.compute.plugins.v3.ips:IPs
keypairs = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs
quota_sets = nova.api.openstack.compute.plugins.v3.quota_sets:QuotaSets