Add availability_zone support for network

This patch adds the availability_zone support for network.

APIImpact
DocImpact

Change-Id: I9259d9679c74d3b3658771290e920a7896631e62
Co-Authored-By: IWAMOTO Toshihiro <iwamoto@valinux.co.jp>
Partially-implements: blueprint add-availability-zone
This commit is contained in:
Hirofumi Ichihara 2015-11-19 15:05:27 +09:00
parent 02db0cd458
commit 6e50027819
21 changed files with 502 additions and 21 deletions

View File

@ -224,6 +224,22 @@
# ports - number of ports associated with the networks hosted on the agent
# dhcp_load_type = networks
# Availability Zone support
#
# Default value of availability zone hints. The availability zone aware
# schedulers use this when the resources availability_zone_hints is empty.
# Multiple availability zones can be specified by a comma separated string.
# This value can be empty. In this case, even if availability_zone_hints for
# a resource is empty, availability zone is considered for high availability
# while scheduling the resource.
# default_availability_zones =
#
# Make network scheduler availability zone aware.
# If multiple availability zones are used, set network_scheduler_driver =
# neutron.scheduler.dhcp_agent_scheduler.AZAwareWeightScheduler
# This scheduler selects agent depending on WeightScheduler logic within an
# availability zone so that considers the weight of agent.
# Allow auto scheduling networks to DHCP agent. It will schedule non-hosted
# networks to first DHCP agent which sends get_active_networks message to
# neutron server

View File

@ -120,6 +120,21 @@ def _validate_string(data, max_len=None):
return msg
def validate_list_of_unique_strings(data, max_string_len=None):
if not isinstance(data, list):
msg = _("'%s' is not a list") % data
return msg
if len(set(data)) != len(data):
msg = _("Duplicate items in the list: '%s'") % ', '.join(data)
return msg
for item in data:
msg = _validate_string(item, max_string_len)
if msg:
return msg
def _validate_boolean(data, valid_values=None):
try:
convert_to_boolean(data)
@ -635,7 +650,8 @@ validators = {'type:dict': _validate_dict,
'type:uuid_or_none': _validate_uuid_or_none,
'type:uuid_list': _validate_uuid_list,
'type:values': _validate_values,
'type:boolean': _validate_boolean}
'type:boolean': _validate_boolean,
'type:list_of_unique_strings': validate_list_of_unique_strings}
# Define constants for base resource name
NETWORK = 'network'

View File

@ -63,6 +63,16 @@ core_opts = [
help=_("The maximum number of items returned in a single "
"response, value was 'infinite' or negative integer "
"means no limit")),
cfg.ListOpt('default_availability_zones', default=[],
help=_("Default value of availability zone hints. The "
"availability zone aware schedulers use this when "
"the resources availability_zone_hints is empty. "
"Multiple availability zones can be specified by a "
"comma separated string. This value can be empty. "
"In this case, even if availability_zone_hints for "
"a resource is empty, availability zone is "
"considered for high availability while scheduling "
"the resource.")),
cfg.IntOpt('max_dns_nameservers', default=5,
help=_("Maximum number of DNS nameservers")),
cfg.IntOpt('max_subnet_host_routes', default=20,

View File

@ -29,6 +29,7 @@ from neutron.common import constants
from neutron.common import utils
from neutron import context as ncontext
from neutron.db import agents_db
from neutron.db.availability_zone import network as network_az
from neutron.db import model_base
from neutron.extensions import agent as ext_agent
from neutron.extensions import dhcpagentscheduler
@ -459,6 +460,21 @@ class DhcpAgentSchedulerDbMixin(dhcpagentscheduler
self.network_scheduler.auto_schedule_networks(self, context, host)
class AZDhcpAgentSchedulerDbMixin(DhcpAgentSchedulerDbMixin,
network_az.NetworkAvailabilityZoneMixin):
"""Mixin class to add availability_zone supported DHCP agent scheduler."""
def get_network_availability_zones(self, network_id):
context = ncontext.get_admin_context()
with context.session.begin():
query = context.session.query(agents_db.Agent.availability_zone)
query = query.join(NetworkDhcpAgentBinding)
query = query.filter(
NetworkDhcpAgentBinding.network_id == network_id)
query = query.group_by(agents_db.Agent.availability_zone)
return [item[0] for item in query]
# helper functions for readability.
def services_available(admin_state_up):
if cfg.CONF.enable_services_on_agents_with_admin_state_down:

View File

View File

@ -0,0 +1,35 @@
#
# 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 oslo_log import log as logging
from neutron.api.v2 import attributes
from neutron.db import common_db_mixin
from neutron.extensions import availability_zone as az_ext
from neutron.extensions import network_availability_zone as net_az
LOG = logging.getLogger(__name__)
class NetworkAvailabilityZoneMixin(net_az.NetworkAvailabilityZonePluginBase):
"""Mixin class to enable network's availability zone attributes."""
def _extend_availability_zone(self, net_res, net_db):
net_res[az_ext.AZ_HINTS] = az_ext.convert_az_string_to_list(
net_db[az_ext.AZ_HINTS])
net_res[az_ext.AVAILABILITY_ZONES] = (
self.get_network_availability_zones(net_db['id']))
common_db_mixin.CommonDbMixin.register_dict_extend_funcs(
attributes.NETWORKS, ['_extend_availability_zone'])

View File

@ -1 +1 @@
32e5974ada25
ec7fcfbf72ee

View File

@ -0,0 +1,33 @@
#
# 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.
#
"""Add network availability zone
Revision ID: ec7fcfbf72ee
Revises: 32e5974ada25
Create Date: 2015-09-17 09:21:51.257579
"""
# revision identifiers, used by Alembic.
revision = 'ec7fcfbf72ee'
down_revision = '32e5974ada25'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('networks',
sa.Column('availability_zone_hints', sa.String(length=255)))

View File

@ -266,3 +266,4 @@ class Network(model_base.HasStandardAttributes, model_base.BASEV2,
rbac_entries = orm.relationship(rbac_db_models.NetworkRBAC,
backref='network', lazy='joined',
cascade='all, delete, delete-orphan')
availability_zone_hints = sa.Column(sa.String(255))

View File

@ -14,6 +14,8 @@
import abc
from oslo_serialization import jsonutils
from neutron.api import extensions
from neutron.api.v2 import attributes as attr
from neutron.api.v2 import base
@ -21,9 +23,36 @@ from neutron.common import exceptions
from neutron import manager
AZ_HINTS_DB_LEN = 255
# resource independent common methods
def convert_az_list_to_string(az_list):
return jsonutils.dumps(az_list)
def convert_az_string_to_list(az_string):
return jsonutils.loads(az_string) if az_string else []
def _validate_availability_zone_hints(data, valid_value=None):
# syntax check only here. existence of az will be checked later.
msg = attr.validate_list_of_unique_strings(data)
if msg:
return msg
az_string = convert_az_list_to_string(data)
if len(az_string) > AZ_HINTS_DB_LEN:
msg = _("Too many availability_zone_hints specified")
raise exceptions.InvalidInput(error_message=msg)
attr.validators['type:availability_zone_hints'] = (
_validate_availability_zone_hints)
# Attribute Map
RESOURCE_NAME = 'availability_zone'
AVAILABILITY_ZONES = 'availability_zones'
AZ_HINTS = 'availability_zone_hints'
# name: name of availability zone (string)
# resource: type of resource: 'network' or 'router'
# state: state of availability zone: 'available' or 'unavailable'
@ -99,9 +128,9 @@ class AvailabilityZonePluginBase(object):
def get_availability_zones(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
pass
"""Return availability zones which a resource belongs to"""
@abc.abstractmethod
def validate_availability_zones(self, context, resource_type,
availability_zones):
pass
"""Verify that the availability zones exist."""

View File

@ -0,0 +1,68 @@
#
# 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 abc
import six
from neutron.api import extensions
from neutron.extensions import availability_zone as az_ext
EXTENDED_ATTRIBUTES_2_0 = {
'networks': {
az_ext.AVAILABILITY_ZONES: {'allow_post': False, 'allow_put': False,
'is_visible': True},
az_ext.AZ_HINTS: {
'allow_post': True, 'allow_put': False, 'is_visible': True,
'validate': {'type:availability_zone_hints': None},
'default': []}},
}
class Network_availability_zone(extensions.ExtensionDescriptor):
"""Network availability zone extension."""
@classmethod
def get_name(cls):
return "Network Availability Zone"
@classmethod
def get_alias(cls):
return "network_availability_zone"
@classmethod
def get_description(cls):
return "Availability zone support for network."
@classmethod
def get_updated(cls):
return "2015-01-01T10:00:00-00:00"
def get_required_extensions(self):
return ["availability_zone"]
def get_extended_resources(self, version):
if version == "2.0":
return EXTENDED_ATTRIBUTES_2_0
else:
return {}
@six.add_metaclass(abc.ABCMeta)
class NetworkAvailabilityZonePluginBase(object):
@abc.abstractmethod
def get_network_availability_zones(self, network_id):
"""Return availability zones which a network belongs to"""

View File

@ -60,6 +60,7 @@ from neutron.db import securitygroups_db
from neutron.db import securitygroups_rpc_base as sg_db_rpc
from neutron.db import vlantransparent_db
from neutron.extensions import allowedaddresspairs as addr_pair
from neutron.extensions import availability_zone as az_ext
from neutron.extensions import extra_dhcp_opt as edo_ext
from neutron.extensions import portbindings
from neutron.extensions import portsecurity as psec
@ -88,7 +89,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
dvr_mac_db.DVRDbMixin,
external_net_db.External_net_db_mixin,
sg_db_rpc.SecurityGroupServerRpcMixin,
agentschedulers_db.DhcpAgentSchedulerDbMixin,
agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
addr_pair_db.AllowedAddressPairsMixin,
vlantransparent_db.Vlantransparent_db_mixin,
extradhcpopt_db.ExtraDhcpOptMixin,
@ -119,7 +120,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
"extra_dhcp_opt", "subnet_allocation",
"net-mtu", "vlan-transparent",
"address-scope", "dns-integration",
"availability_zone"]
"availability_zone",
"network_availability_zone"]
@property
def supported_extension_aliases(self):
@ -641,6 +643,15 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
result['id'], {'network': {api.MTU: net_data[api.MTU]}})
result[api.MTU] = res.get(api.MTU, 0)
if az_ext.AZ_HINTS in net_data:
self.validate_availability_zones(context, 'network',
net_data[az_ext.AZ_HINTS])
az_hints = az_ext.convert_az_list_to_string(
net_data[az_ext.AZ_HINTS])
super(Ml2Plugin, self).update_network(context,
result['id'], {'network': {az_ext.AZ_HINTS: az_hints}})
result[az_ext.AZ_HINTS] = az_hints
return result, mech_context
def create_network(self, context, network):

View File

@ -14,6 +14,9 @@
# under the License.
import collections
import heapq
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log as logging
@ -22,6 +25,7 @@ from sqlalchemy import sql
from neutron.common import constants
from neutron.db import agents_db
from neutron.db import agentschedulers_db
from neutron.extensions import availability_zone as az_ext
from neutron.i18n import _LI, _LW
from neutron.scheduler import base_resource_filter
from neutron.scheduler import base_scheduler
@ -64,6 +68,12 @@ class AutoScheduler(object):
continue
if any(dhcp_agent.id == agent.id for agent in agents):
continue
net = plugin.get_network(context, net_id)
az_hints = (net.get(az_ext.AZ_HINTS) or
cfg.CONF.default_availability_zones)
if (az_hints and
dhcp_agent['availability_zone'] not in az_hints):
continue
bindings_to_add.append((dhcp_agent, net_id))
# do it outside transaction so particular scheduling results don't
# make other to fail
@ -84,6 +94,46 @@ class WeightScheduler(base_scheduler.BaseWeightScheduler, AutoScheduler):
super(WeightScheduler, self).__init__(DhcpFilter())
class AZAwareWeightScheduler(WeightScheduler):
def select(self, plugin, context, resource_hostable_agents,
resource_hosted_agents, num_agents_needed):
"""AZ aware scheduling
If the network has multiple AZs, agents are scheduled as
follows:
- select AZ with least agents scheduled for the network
(nondeterministic for AZs with same amount of agents scheduled)
- choose agent in the AZ with WeightScheduler
"""
hostable_az_agents = collections.defaultdict(list)
num_az_agents = {}
for agent in resource_hostable_agents:
az_agent = agent['availability_zone']
hostable_az_agents[az_agent].append(agent)
if az_agent not in num_az_agents:
num_az_agents[az_agent] = 0
if num_agents_needed <= 0:
return []
for agent in resource_hosted_agents:
az_agent = agent['availability_zone']
if az_agent in num_az_agents:
num_az_agents[az_agent] += 1
num_az_q = [(value, key) for key, value in num_az_agents.items()]
heapq.heapify(num_az_q)
chosen_agents = []
while num_agents_needed > 0:
num, select_az = heapq.heappop(num_az_q)
select_agent = super(AZAwareWeightScheduler, self).select(
plugin, context, hostable_az_agents[select_az], [], 1)
chosen_agents.append(select_agent[0])
hostable_az_agents[select_az].remove(select_agent[0])
if hostable_az_agents[select_az]:
heapq.heappush(num_az_q, (num + 1, select_az))
num_agents_needed -= 1
return chosen_agents
class DhcpFilter(base_resource_filter.BaseResourceFilter):
def bind(self, context, agents, network_id):
@ -147,13 +197,15 @@ class DhcpFilter(base_resource_filter.BaseResourceFilter):
return
return network_hosted_agents
def _get_active_agents(self, plugin, context):
def _get_active_agents(self, plugin, context, az_hints):
"""Return a list of active dhcp agents."""
with context.session.begin(subtransactions=True):
filters = {'agent_type': [constants.AGENT_TYPE_DHCP],
'admin_state_up': [True]}
if az_hints:
filters['availability_zone'] = az_hints
active_dhcp_agents = plugin.get_agents_db(
context, filters={
'agent_type': [constants.AGENT_TYPE_DHCP],
'admin_state_up': [True]})
context, filters=filters)
if not active_dhcp_agents:
LOG.warn(_LW('No more DHCP agents'))
return []
@ -171,7 +223,9 @@ class DhcpFilter(base_resource_filter.BaseResourceFilter):
if hosted_agents is None:
return {'n_agents': 0, 'hostable_agents': [], 'hosted_agents': []}
n_agents = cfg.CONF.dhcp_agents_per_network - len(hosted_agents)
active_dhcp_agents = self._get_active_agents(plugin, context)
az_hints = (network.get(az_ext.AZ_HINTS) or
cfg.CONF.default_availability_zones)
active_dhcp_agents = self._get_active_agents(plugin, context, az_hints)
if not active_dhcp_agents:
return {'n_agents': 0, 'hostable_agents': [],
'hosted_agents': hosted_agents}

View File

@ -345,6 +345,10 @@ class TestAutoSchedule(test_dhcp_sch.TestDhcpSchedulerBaseTestCase,
'enable_dhcp': enable_dhcp})
return subnets
def get_network(self, context, net_id):
# TODO(hichihara): add test cases of AZ scheduler
return {'availability_zone_hints': []}
def _get_hosted_networks_on_dhcp_agent(self, agent_id):
query = self.ctx.session.query(
agentschedulers_db.NetworkDhcpAgentBinding.network_id)

View File

@ -113,6 +113,24 @@ class TestAttributes(base.BaseTestCase):
msg = attributes._validate_string("123456789", None)
self.assertIsNone(msg)
def test_validate_list_of_unique_strings(self):
data = "TEST"
msg = attributes.validate_list_of_unique_strings(data, None)
self.assertEqual("'TEST' is not a list", msg)
data = ["TEST01", "TEST02", "TEST01"]
msg = attributes.validate_list_of_unique_strings(data, None)
self.assertEqual(
"Duplicate items in the list: 'TEST01, TEST02, TEST01'", msg)
data = ["12345678", "123456789"]
msg = attributes.validate_list_of_unique_strings(data, 8)
self.assertEqual("'123456789' exceeds maximum length of 8", msg)
data = ["TEST01", "TEST02", "TEST03"]
msg = attributes.validate_list_of_unique_strings(data, None)
self.assertIsNone(msg)
def test_validate_no_whitespace(self):
data = 'no_white_space'
result = attributes._validate_no_whitespace(data)

View File

@ -292,7 +292,8 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
'admin_state_up': admin_state_up,
'tenant_id': self._tenant_id}}
for arg in (('admin_state_up', 'tenant_id', 'shared',
'vlan_transparent') + (arg_list or ())):
'vlan_transparent',
'availability_zone_hints') + (arg_list or ())):
# Arg must be present
if arg in kwargs:
data['network'][arg] = kwargs[arg]
@ -5610,7 +5611,9 @@ class DbModelTestCase(testlib_api.SqlTestCase):
exp_end_with = (" {tenant_id=None, id=None, "
"name='net_net', status='OK', "
"admin_state_up=True, mtu=None, "
"vlan_transparent=None, standard_attr_id=None}>")
"vlan_transparent=None, "
"availability_zone_hints=None, "
"standard_attr_id=None}>")
final_exp = exp_start_with + exp_middle + exp_end_with
self.assertEqual(final_exp, actual_repr_output)

View File

@ -40,7 +40,6 @@ class AZExtensionManager(object):
return []
# This plugin class is just for testing
class AZTestPlugin(db_base_plugin_v2.NeutronDbPluginV2,
agents_db.AgentDbMixin):
supported_extension_aliases = ["agent", "availability_zone"]
@ -96,3 +95,41 @@ class TestAZAgentCase(AZTestCommon):
self.assertRaises(az_ext.AvailabilityZoneNotFound,
self.plugin.validate_availability_zones,
ctx, 'router', ['nova1'])
class TestAZNetworkCase(AZTestCommon):
def setUp(self):
plugin = 'neutron.plugins.ml2.plugin.Ml2Plugin'
ext_mgr = AZExtensionManager()
super(TestAZNetworkCase, self).setUp(plugin=plugin, ext_mgr=ext_mgr)
def test_create_network_with_az(self):
self._register_azs()
az_hints = ['nova1']
with self.network(availability_zone_hints=az_hints) as net:
res = self._show('networks', net['network']['id'])
self.assertItemsEqual(az_hints,
res['network']['availability_zone_hints'])
def test_create_network_with_azs(self):
self._register_azs()
az_hints = ['nova1', 'nova2']
with self.network(availability_zone_hints=az_hints) as net:
res = self._show('networks', net['network']['id'])
self.assertItemsEqual(az_hints,
res['network']['availability_zone_hints'])
def test_create_network_without_az(self):
with self.network() as net:
res = self._show('networks', net['network']['id'])
self.assertEqual([], res['network']['availability_zone_hints'])
def test_create_network_with_empty_az(self):
with self.network(availability_zone_hints=[]) as net:
res = self._show('networks', net['network']['id'])
self.assertEqual([], res['network']['availability_zone_hints'])
def test_create_network_with_not_exist_az(self):
res = self._create_network(self.fmt, 'net', True,
availability_zone_hints=['nova3'])
self.assertEqual(404, res.status_int)

View File

@ -80,7 +80,7 @@ class ExtensionDriverTestCase(test_plugin.Ml2PluginV2TestCase):
def test_faulty_extend_dict(self):
with mock.patch.object(ext_test.TestExtensionDriver,
'extend_network_dict',
side_effect=TypeError):
side_effect=[None, TypeError]):
network, tid = self._verify_network_create(201, None)
self._verify_network_update(network, 400, 'ExtensionDriverError')

View File

@ -1667,6 +1667,7 @@ class TestMl2PluginCreateUpdateDeletePort(base.BaseTestCase):
plugin._get_host_port_if_changed = mock.Mock(
return_value=new_host_port)
plugin._check_mac_update_allowed = mock.Mock(return_value=True)
plugin._extend_availability_zone = mock.Mock()
self.notify.side_effect = (
lambda r, e, t, **kwargs: self._ensure_transaction_is_closed())

View File

@ -135,6 +135,7 @@ class TestDhcpScheduler(TestDhcpSchedulerBaseTestCase):
plugin = mock.Mock()
plugin.get_subnets.return_value = [{"network_id": self.network_id,
"enable_dhcp": True}]
plugin.get_network.return_value = self.network
if active_hosts_only:
plugin.get_dhcp_agents_hosting_networks.return_value = []
else:
@ -180,6 +181,10 @@ class TestAutoScheduleNetworks(TestDhcpSchedulerBaseTestCase):
valid_host
If true, then an valid host is passed to schedule the network,
else an invalid host is passed.
az_hints
'availability_zone_hints' of the network.
note that default 'availability_zone' of an agent is 'nova'.
"""
scenarios = [
('Network present',
@ -187,42 +192,64 @@ class TestAutoScheduleNetworks(TestDhcpSchedulerBaseTestCase):
enable_dhcp=True,
scheduled_already=False,
agent_down=False,
valid_host=True)),
valid_host=True,
az_hints=[])),
('No network',
dict(network_present=False,
enable_dhcp=False,
scheduled_already=False,
agent_down=False,
valid_host=True)),
valid_host=True,
az_hints=[])),
('Network already scheduled',
dict(network_present=True,
enable_dhcp=True,
scheduled_already=True,
agent_down=False,
valid_host=True)),
valid_host=True,
az_hints=[])),
('Agent down',
dict(network_present=True,
enable_dhcp=True,
scheduled_already=False,
agent_down=False,
valid_host=True)),
valid_host=True,
az_hints=[])),
('dhcp disabled',
dict(network_present=True,
enable_dhcp=False,
scheduled_already=False,
agent_down=False,
valid_host=False)),
valid_host=False,
az_hints=[])),
('Invalid host',
dict(network_present=True,
enable_dhcp=True,
scheduled_already=False,
agent_down=False,
valid_host=False)),
valid_host=False,
az_hints=[])),
('Match AZ',
dict(network_present=True,
enable_dhcp=True,
scheduled_already=False,
agent_down=False,
valid_host=True,
az_hints=['nova'])),
('Not match AZ',
dict(network_present=True,
enable_dhcp=True,
scheduled_already=False,
agent_down=False,
valid_host=True,
az_hints=['not-match'])),
]
def test_auto_schedule_network(self):
@ -230,6 +257,8 @@ class TestAutoScheduleNetworks(TestDhcpSchedulerBaseTestCase):
plugin.get_subnets.return_value = (
[{"network_id": self.network_id, "enable_dhcp": self.enable_dhcp}]
if self.network_present else [])
plugin.get_network.return_value = {'availability_zone_hints':
self.az_hints}
scheduler = dhcp_agent_scheduler.ChanceScheduler()
if self.network_present:
down_agent_count = 1 if self.agent_down else 0
@ -241,6 +270,9 @@ class TestAutoScheduleNetworks(TestDhcpSchedulerBaseTestCase):
expected_result = (self.network_present and self.enable_dhcp)
expected_hosted_agents = (1 if expected_result and
self.valid_host else 0)
if (self.az_hints and
agents[0]['availability_zone'] not in self.az_hints):
expected_hosted_agents = 0
host = "host-a" if self.valid_host else "host-b"
observed_ret_value = scheduler.auto_schedule_networks(
plugin, self.ctx, host)
@ -448,3 +480,96 @@ class TestDhcpSchedulerFilter(TestDhcpSchedulerBaseTestCase,
self._test_get_dhcp_agents_hosting_networks({'host-d'},
active=True,
admin_state_up=False)
class DHCPAgentAZAwareWeightSchedulerTestCase(TestDhcpSchedulerBaseTestCase):
def setUp(self):
super(DHCPAgentAZAwareWeightSchedulerTestCase, self).setUp()
DB_PLUGIN_KLASS = 'neutron.plugins.ml2.plugin.Ml2Plugin'
self.setup_coreplugin(DB_PLUGIN_KLASS)
cfg.CONF.set_override("network_scheduler_driver",
'neutron.scheduler.dhcp_agent_scheduler.AZAwareWeightScheduler')
self.plugin = importutils.import_object('neutron.plugins.ml2.plugin.'
'Ml2Plugin')
cfg.CONF.set_override('dhcp_agents_per_network', 1)
cfg.CONF.set_override("dhcp_load_type", "networks")
def test_az_scheduler_one_az_hints(self):
self._save_networks(['1111'])
helpers.register_dhcp_agent('az1-host1', networks=1, az='az1')
helpers.register_dhcp_agent('az1-host2', networks=2, az='az1')
helpers.register_dhcp_agent('az2-host1', networks=3, az='az2')
helpers.register_dhcp_agent('az2-host2', networks=4, az='az2')
self.plugin.network_scheduler.schedule(self.plugin, self.ctx,
{'id': '1111', 'availability_zone_hints': ['az2']})
agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
['1111'])
self.assertEqual(1, len(agents))
self.assertEqual('az2-host1', agents[0]['host'])
def test_az_scheduler_default_az_hints(self):
cfg.CONF.set_override('default_availability_zones', ['az1'])
self._save_networks(['1111'])
helpers.register_dhcp_agent('az1-host1', networks=1, az='az1')
helpers.register_dhcp_agent('az1-host2', networks=2, az='az1')
helpers.register_dhcp_agent('az2-host1', networks=3, az='az2')
helpers.register_dhcp_agent('az2-host2', networks=4, az='az2')
self.plugin.network_scheduler.schedule(self.plugin, self.ctx,
{'id': '1111', 'availability_zone_hints': []})
agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
['1111'])
self.assertEqual(1, len(agents))
self.assertEqual('az1-host1', agents[0]['host'])
def test_az_scheduler_two_az_hints(self):
cfg.CONF.set_override('dhcp_agents_per_network', 2)
self._save_networks(['1111'])
helpers.register_dhcp_agent('az1-host1', networks=1, az='az1')
helpers.register_dhcp_agent('az1-host2', networks=2, az='az1')
helpers.register_dhcp_agent('az2-host1', networks=3, az='az2')
helpers.register_dhcp_agent('az2-host2', networks=4, az='az2')
helpers.register_dhcp_agent('az3-host1', networks=5, az='az3')
helpers.register_dhcp_agent('az3-host2', networks=6, az='az3')
self.plugin.network_scheduler.schedule(self.plugin, self.ctx,
{'id': '1111', 'availability_zone_hints': ['az1', 'az3']})
agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
['1111'])
self.assertEqual(2, len(agents))
expected_hosts = set(['az1-host1', 'az3-host1'])
hosts = set([a['host'] for a in agents])
self.assertEqual(expected_hosts, hosts)
def test_az_scheduler_two_az_hints_one_available_az(self):
cfg.CONF.set_override('dhcp_agents_per_network', 2)
self._save_networks(['1111'])
helpers.register_dhcp_agent('az1-host1', networks=1, az='az1')
helpers.register_dhcp_agent('az1-host2', networks=2, az='az1')
helpers.register_dhcp_agent('az2-host1', networks=3, alive=False,
az='az2')
helpers.register_dhcp_agent('az2-host2', networks=4,
admin_state_up=False, az='az2')
self.plugin.network_scheduler.schedule(self.plugin, self.ctx,
{'id': '1111', 'availability_zone_hints': ['az1', 'az2']})
agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
['1111'])
self.assertEqual(2, len(agents))
expected_hosts = set(['az1-host1', 'az1-host2'])
hosts = set([a['host'] for a in agents])
self.assertEqual(expected_hosts, hosts)
def test_az_scheduler_no_az_hints(self):
cfg.CONF.set_override('dhcp_agents_per_network', 2)
self._save_networks(['1111'])
helpers.register_dhcp_agent('az1-host1', networks=2, az='az1')
helpers.register_dhcp_agent('az1-host2', networks=3, az='az1')
helpers.register_dhcp_agent('az2-host1', networks=2, az='az2')
helpers.register_dhcp_agent('az2-host2', networks=1, az='az2')
self.plugin.network_scheduler.schedule(self.plugin, self.ctx,
{'id': '1111', 'availability_zone_hints': []})
agents = self.plugin.get_dhcp_agents_hosting_networks(self.ctx,
['1111'])
self.assertEqual(2, len(agents))
expected_hosts = set(['az1-host1', 'az2-host2'])
hosts = {a['host'] for a in agents}
self.assertEqual(expected_hosts, hosts)

View File

@ -0,0 +1,4 @@
---
features:
- DHCP agent is assigned to a availability zone. Network can be host on the
DHCP agent with availability zone which users specify.