summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSandy Walsh <sandy.walsh@rackspace.com>2011-06-30 18:23:06 +0000
committerTarmac <>2011-06-30 18:23:06 +0000
commit88a045916b6ead2f368b166a563eac45d1305035 (patch)
treeed63e16b4d9a2114109b07f751e9a04a8aa9592c
parentbf09a9e63f33c4cd9a65a9b2464f0049625ac024 (diff)
parent6243f715c9bab947fc18501aecc4b15b7e3ff93c (diff)
downloadnova-88a045916b6ead2f368b166a563eac45d1305035.tar.gz
nova-88a045916b6ead2f368b166a563eac45d1305035.tar.xz
nova-88a045916b6ead2f368b166a563eac45d1305035.zip
Child Zone Weight adjustment available when adding Child Zones.
-rw-r--r--nova/compute/api.py56
-rw-r--r--nova/compute/manager.py1
-rw-r--r--nova/db/api.py2
-rw-r--r--nova/db/sqlalchemy/api.py2
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/029_add_zone_weight_offsets.py38
-rw-r--r--nova/db/sqlalchemy/models.py4
-rw-r--r--nova/scheduler/api.py7
-rw-r--r--nova/scheduler/zone_aware_scheduler.py79
-rw-r--r--nova/tests/api/openstack/test_zones.py10
-rw-r--r--nova/tests/scheduler/test_zone_aware_scheduler.py44
10 files changed, 183 insertions, 60 deletions
diff --git a/nova/compute/api.py b/nova/compute/api.py
index a7d0f7e62..e268c9b45 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -225,18 +225,7 @@ class API(base.Base):
if ramdisk_id:
image_service.show(context, ramdisk_id)
- if security_group is None:
- security_group = ['default']
- if not type(security_group) is list:
- security_group = [security_group]
-
- security_groups = []
self.ensure_default_security_group(context)
- for security_group_name in security_group:
- group = db.security_group_get_by_name(context,
- context.project_id,
- security_group_name)
- security_groups.append(group['id'])
if key_data is None and key_name:
key_pair = db.key_pair_get(context, context.user_id, key_name)
@@ -271,15 +260,19 @@ class API(base.Base):
'architecture': architecture,
'vm_mode': vm_mode}
- return (num_instances, base_options, security_groups)
+ return (num_instances, base_options)
def create_db_entry_for_new_instance(self, context, base_options,
- security_groups, block_device_mapping, num=1):
+ security_group, block_device_mapping, num=1):
"""Create an entry in the DB for this new instance,
- including any related table updates (such as security
- groups, MAC address, etc). This will called by create()
- in the majority of situations, but all-at-once style
- Schedulers may initiate the call."""
+ including any related table updates (such as security group,
+ MAC address, etc).
+
+ This will called by create() in the majority of situations,
+ but create_all_at_once() style Schedulers may initiate the call.
+ If you are changing this method, be sure to update both
+ call paths.
+ """
instance = dict(mac_address=utils.generate_mac(),
launch_index=num,
**base_options)
@@ -287,13 +280,24 @@ class API(base.Base):
instance_id = instance['id']
elevated = context.elevated()
- if not security_groups:
- security_groups = []
+ if security_group is None:
+ security_group = ['default']
+ if not isinstance(security_group, list):
+ security_group = [security_group]
+
+ security_groups = []
+ for security_group_name in security_group:
+ group = db.security_group_get_by_name(context,
+ context.project_id,
+ security_group_name)
+ security_groups.append(group['id'])
+
for security_group_id in security_groups:
self.db.instance_add_security_group(elevated,
instance_id,
security_group_id)
+ block_device_mapping = block_device_mapping or []
# NOTE(yamahata)
# tell vm driver to attach volume at boot time by updating
# BlockDeviceMapping
@@ -367,12 +371,11 @@ class API(base.Base):
key_name=None, key_data=None, security_group='default',
availability_zone=None, user_data=None, metadata={},
injected_files=None, admin_password=None, zone_blob=None,
- reservation_id=None):
+ reservation_id=None, block_device_mapping=None):
"""Provision the instances by passing the whole request to
the Scheduler for execution. Returns a Reservation ID
related to the creation of all of these instances."""
- num_instances, base_options, security_groups = \
- self._check_create_parameters(
+ num_instances, base_options = self._check_create_parameters(
context, instance_type,
image_href, kernel_id, ramdisk_id,
min_count, max_count,
@@ -404,11 +407,13 @@ class API(base.Base):
Scheduler drivers, but may remove the effectiveness of the
more complicated drivers.
+ NOTE: If you change this method, be sure to change
+ create_all_at_once() at the same time!
+
Returns a list of instance dicts.
"""
- num_instances, base_options, security_groups = \
- self._check_create_parameters(
+ num_instances, base_options = self._check_create_parameters(
context, instance_type,
image_href, kernel_id, ramdisk_id,
min_count, max_count,
@@ -418,12 +423,11 @@ class API(base.Base):
injected_files, admin_password, zone_blob,
reservation_id)
- block_device_mapping = block_device_mapping or []
instances = []
LOG.debug(_("Going to run %s instances..."), num_instances)
for num in range(num_instances):
instance = self.create_db_entry_for_new_instance(context,
- base_options, security_groups,
+ base_options, security_group,
block_device_mapping, num=num)
instances.append(instance)
instance_id = instance['id']
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index e7f31d8ca..eac4fba72 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -50,7 +50,6 @@ import nova.image
from nova import log as logging
from nova import manager
from nova import network
-from nova import notifier
from nova import rpc
from nova import utils
from nova import volume
diff --git a/nova/db/api.py b/nova/db/api.py
index 72e967610..f4450fcb9 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -1289,7 +1289,7 @@ def zone_create(context, values):
def zone_update(context, zone_id, values):
"""Update a child Zone entry."""
- return IMPL.zone_update(context, values)
+ return IMPL.zone_update(context, zone_id, values)
def zone_delete(context, zone_id):
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 6bd16d42e..822a17a63 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -2812,7 +2812,7 @@ def zone_update(context, zone_id, values):
if not zone:
raise exception.ZoneNotFound(zone_id=zone_id)
zone.update(values)
- zone.save()
+ zone.save(session=session)
return zone
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/029_add_zone_weight_offsets.py b/nova/db/sqlalchemy/migrate_repo/versions/029_add_zone_weight_offsets.py
new file mode 100644
index 000000000..1b7871e5f
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/029_add_zone_weight_offsets.py
@@ -0,0 +1,38 @@
+# Copyright 2011 OpenStack LLC.
+#
+# 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 sqlalchemy import Column, Float, Integer, MetaData, Table
+
+meta = MetaData()
+
+zones = Table('zones', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+weight_offset = Column('weight_offset', Float(), default=0.0)
+weight_scale = Column('weight_scale', Float(), default=1.0)
+
+
+def upgrade(migrate_engine):
+ meta.bind = migrate_engine
+
+ zones.create_column(weight_offset)
+ zones.create_column(weight_scale)
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+
+ zones.drop_column(weight_offset)
+ zones.drop_column(weight_scale)
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 2955b8780..bb659b08c 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -21,7 +21,7 @@ SQLAlchemy models for nova data.
from sqlalchemy.orm import relationship, backref, object_mapper
from sqlalchemy import Column, Integer, String, schema
-from sqlalchemy import ForeignKey, DateTime, Boolean, Text
+from sqlalchemy import ForeignKey, DateTime, Boolean, Text, Float
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import ForeignKeyConstraint
@@ -738,6 +738,8 @@ class Zone(BASE, NovaBase):
api_url = Column(String(255))
username = Column(String(255))
password = Column(String(255))
+ weight_offset = Column(Float(), default=0.0)
+ weight_scale = Column(Float(), default=1.0)
class AgentBuild(BASE, NovaBase):
diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py
index 5d62beb86..0f4fc48c8 100644
--- a/nova/scheduler/api.py
+++ b/nova/scheduler/api.py
@@ -114,7 +114,8 @@ def _process(func, zone):
def call_zone_method(context, method_name, errors_to_ignore=None,
- novaclient_collection_name='zones', *args, **kwargs):
+ novaclient_collection_name='zones', zones=None,
+ *args, **kwargs):
"""Returns a list of (zone, call_result) objects."""
if not isinstance(errors_to_ignore, (list, tuple)):
# This will also handle the default None
@@ -122,7 +123,9 @@ def call_zone_method(context, method_name, errors_to_ignore=None,
pool = greenpool.GreenPool()
results = []
- for zone in db.zone_get_all(context):
+ if zones is None:
+ zones = db.zone_get_all(context)
+ for zone in zones:
try:
nova = novaclient.OpenStack(zone.username, zone.password, None,
zone.api_url)
diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py
index 9e4fc47d1..1cc98e48b 100644
--- a/nova/scheduler/zone_aware_scheduler.py
+++ b/nova/scheduler/zone_aware_scheduler.py
@@ -33,6 +33,7 @@ from nova import flags
from nova import log as logging
from nova import rpc
+from nova.compute import api as compute_api
from nova.scheduler import api
from nova.scheduler import driver
@@ -48,14 +49,25 @@ class InvalidBlob(exception.NovaException):
class ZoneAwareScheduler(driver.Scheduler):
"""Base class for creating Zone Aware Schedulers."""
- def _call_zone_method(self, context, method, specs):
+ def _call_zone_method(self, context, method, specs, zones):
"""Call novaclient zone method. Broken out for testing."""
- return api.call_zone_method(context, method, specs=specs)
+ return api.call_zone_method(context, method, specs=specs, zones=zones)
- def _provision_resource_locally(self, context, item, instance_id, kwargs):
+ def _provision_resource_locally(self, context, build_plan_item,
+ request_spec, kwargs):
"""Create the requested resource in this Zone."""
- host = item['hostname']
+ host = build_plan_item['hostname']
+ base_options = request_spec['instance_properties']
+
+ # TODO(sandy): I guess someone needs to add block_device_mapping
+ # support at some point? Also, OS API has no concept of security
+ # groups.
+ instance = compute_api.API().create_db_entry_for_new_instance(context,
+ base_options, None, [])
+
+ instance_id = instance['id']
kwargs['instance_id'] = instance_id
+
rpc.cast(context,
db.queue_get_for(context, "compute", host),
{"method": "run_instance",
@@ -115,8 +127,8 @@ class ZoneAwareScheduler(driver.Scheduler):
nova.servers.create(name, image_ref, flavor_id, ipgroup, meta, files,
child_blob, reservation_id=reservation_id)
- def _provision_resource_from_blob(self, context, item, instance_id,
- request_spec, kwargs):
+ def _provision_resource_from_blob(self, context, build_plan_item,
+ instance_id, request_spec, kwargs):
"""Create the requested resource locally or in a child zone
based on what is stored in the zone blob info.
@@ -132,12 +144,12 @@ class ZoneAwareScheduler(driver.Scheduler):
request."""
host_info = None
- if "blob" in item:
+ if "blob" in build_plan_item:
# Request was passed in from above. Is it for us?
- host_info = self._decrypt_blob(item['blob'])
- elif "child_blob" in item:
+ host_info = self._decrypt_blob(build_plan_item['blob'])
+ elif "child_blob" in build_plan_item:
# Our immediate child zone provided this info ...
- host_info = item
+ host_info = build_plan_item
if not host_info:
raise InvalidBlob()
@@ -147,19 +159,44 @@ class ZoneAwareScheduler(driver.Scheduler):
self._ask_child_zone_to_create_instance(context, host_info,
request_spec, kwargs)
else:
- self._provision_resource_locally(context, host_info,
- instance_id, kwargs)
+ self._provision_resource_locally(context, host_info, request_spec,
+ kwargs)
- def _provision_resource(self, context, item, instance_id, request_spec,
- kwargs):
+ def _provision_resource(self, context, build_plan_item, instance_id,
+ request_spec, kwargs):
"""Create the requested resource in this Zone or a child zone."""
- if "hostname" in item:
- self._provision_resource_locally(context, item, instance_id,
- kwargs)
+ if "hostname" in build_plan_item:
+ self._provision_resource_locally(context, build_plan_item,
+ request_spec, kwargs)
return
- self._provision_resource_from_blob(context, item, instance_id,
- request_spec, kwargs)
+ self._provision_resource_from_blob(context, build_plan_item,
+ instance_id, request_spec, kwargs)
+
+ def _adjust_child_weights(self, child_results, zones):
+ """Apply the Scale and Offset values from the Zone definition
+ to adjust the weights returned from the child zones. Alters
+ child_results in place.
+ """
+ for zone, result in child_results:
+ if not result:
+ continue
+
+ for zone_rec in zones:
+ if zone_rec['api_url'] != zone:
+ continue
+
+ for item in result:
+ try:
+ offset = zone_rec['weight_offset']
+ scale = zone_rec['weight_scale']
+ raw_weight = item['weight']
+ cooked_weight = offset + scale * raw_weight
+ item['weight'] = cooked_weight
+ item['raw_weight'] = raw_weight
+ except KeyError:
+ LOG.exception(_("Bad child zone scaling values "
+ "for Zone: %(zone)s") % locals())
def schedule_run_instance(self, context, instance_id, request_spec,
*args, **kwargs):
@@ -261,8 +298,10 @@ class ZoneAwareScheduler(driver.Scheduler):
# Next, tack on the best weights from the child zones ...
json_spec = json.dumps(request_spec)
+ all_zones = db.zone_get_all(context)
child_results = self._call_zone_method(context, "select",
- specs=json_spec)
+ specs=json_spec, zones=all_zones)
+ self._adjust_child_weights(child_results, all_zones)
for child_zone, result in child_results:
for weighting in result:
# Remember the child_zone so we can get back to
diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py
index 098577e4c..6a6e13d93 100644
--- a/nova/tests/api/openstack/test_zones.py
+++ b/nova/tests/api/openstack/test_zones.py
@@ -34,7 +34,7 @@ FLAGS.verbose = True
def zone_get(context, zone_id):
return dict(id=1, api_url='http://example.com', username='bob',
- password='xxx')
+ password='xxx', weight_scale=1.0, weight_offset=0.0)
def zone_create(context, values):
@@ -57,9 +57,9 @@ def zone_delete(context, zone_id):
def zone_get_all_scheduler(*args):
return [
dict(id=1, api_url='http://example.com', username='bob',
- password='xxx'),
+ password='xxx', weight_scale=1.0, weight_offset=0.0),
dict(id=2, api_url='http://example.org', username='alice',
- password='qwerty'),
+ password='qwerty', weight_scale=1.0, weight_offset=0.0),
]
@@ -70,9 +70,9 @@ def zone_get_all_scheduler_empty(*args):
def zone_get_all_db(context):
return [
dict(id=1, api_url='http://example.com', username='bob',
- password='xxx'),
+ password='xxx', weight_scale=1.0, weight_offset=0.0),
dict(id=2, api_url='http://example.org', username='alice',
- password='qwerty'),
+ password='qwerty', weight_scale=1.0, weight_offset=0.0),
]
diff --git a/nova/tests/scheduler/test_zone_aware_scheduler.py b/nova/tests/scheduler/test_zone_aware_scheduler.py
index 32f5150a5..5950f4551 100644
--- a/nova/tests/scheduler/test_zone_aware_scheduler.py
+++ b/nova/tests/scheduler/test_zone_aware_scheduler.py
@@ -16,6 +16,8 @@
Tests For Zone Aware Scheduler.
"""
+import nova.db
+
from nova import exception
from nova import test
from nova.scheduler import driver
@@ -79,7 +81,7 @@ class FakeEmptyZoneManager(zone_manager.ZoneManager):
self.service_states = {}
-def fake_empty_call_zone_method(context, method, specs):
+def fake_empty_call_zone_method(context, method, specs, zones):
return []
@@ -98,7 +100,7 @@ def fake_ask_child_zone_to_create_instance(context, zone_info,
was_called = True
-def fake_provision_resource_locally(context, item, instance_id, kwargs):
+def fake_provision_resource_locally(context, build_plan, request_spec, kwargs):
global was_called
was_called = True
@@ -118,7 +120,7 @@ def fake_decrypt_blob_returns_child_info(blob):
'child_blob': True} # values aren't important. Keys are.
-def fake_call_zone_method(context, method, specs):
+def fake_call_zone_method(context, method, specs, zones):
return [
('zone1', [
dict(weight=1, blob='AAAAAAA'),
@@ -141,6 +143,20 @@ def fake_call_zone_method(context, method, specs):
]
+def fake_zone_get_all(context):
+ return [
+ dict(id=1, api_url='zone1',
+ username='admin', password='password',
+ weight_offset=0.0, weight_scale=1.0),
+ dict(id=2, api_url='zone2',
+ username='admin', password='password',
+ weight_offset=1000.0, weight_scale=1.0),
+ dict(id=3, api_url='zone3',
+ username='admin', password='password',
+ weight_offset=0.0, weight_scale=1000.0),
+ ]
+
+
class ZoneAwareSchedulerTestCase(test.TestCase):
"""Test case for Zone Aware Scheduler."""
@@ -151,6 +167,7 @@ class ZoneAwareSchedulerTestCase(test.TestCase):
"""
sched = FakeZoneAwareScheduler()
self.stubs.Set(sched, '_call_zone_method', fake_call_zone_method)
+ self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all)
zm = FakeZoneManager()
sched.set_zone_manager(zm)
@@ -168,12 +185,33 @@ class ZoneAwareSchedulerTestCase(test.TestCase):
# 4 local hosts
self.assertEqual(4, len(hostnames))
+ def test_adjust_child_weights(self):
+ """Make sure the weights returned by child zones are
+ properly adjusted based on the scale/offset in the zone
+ db entries.
+ """
+ sched = FakeZoneAwareScheduler()
+ child_results = fake_call_zone_method(None, None, None, None)
+ zones = fake_zone_get_all(None)
+ sched._adjust_child_weights(child_results, zones)
+ scaled = [130000, 131000, 132000, 3000]
+ for zone, results in child_results:
+ for item in results:
+ w = item['weight']
+ if zone == 'zone1': # No change
+ self.assertTrue(w < 1000.0)
+ if zone == 'zone2': # Offset +1000
+ self.assertTrue(w >= 1000.0 and w < 2000)
+ if zone == 'zone3': # Scale x1000
+ self.assertEqual(scaled.pop(0), w)
+
def test_empty_zone_aware_scheduler(self):
"""
Ensure empty hosts & child_zones result in NoValidHosts exception.
"""
sched = FakeZoneAwareScheduler()
self.stubs.Set(sched, '_call_zone_method', fake_empty_call_zone_method)
+ self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all)
zm = FakeEmptyZoneManager()
sched.set_zone_manager(zm)