Add availability_zone support base

This patch adds the availability_zone attribute to agents and
supports availability_zone API.
Availability_zone support for resources (network/router) and
the schedulers are included in subsequent patches.

APIImpact
DocImpact

Co-Authored-By: IWAMOTO Toshihiro <iwamoto@valinux.co.jp>
Change-Id: Id7a62000ab0484412b3970199df8c374568fe70d
Partially-implements: blueprint add-availability-zone
This commit is contained in:
Hirofumi Ichihara 2015-10-02 10:14:10 +09:00
parent afabab7fd1
commit 9c2c3021a6
14 changed files with 358 additions and 9 deletions

View File

@ -706,6 +706,9 @@
# exit - Exits the agent
# check_child_processes_action = respawn
# Availability zone of this node.
# availability_zone = nova
# =========== items for agent management extension =============
# seconds between nodes reporting state to server; should be less than
# agent_down_time, best if it is half or less than agent_down_time

View File

@ -16,6 +16,7 @@
import os
from oslo_config import cfg
from oslo_config import types
from oslo_log import log as logging
from neutron.common import config
@ -75,6 +76,37 @@ PROCESS_MONITOR_OPTS = [
]
# TODO(hichihara): Remove these two classes, once oslo fixes types.string
# and cfg.StrOpt.
class LengthString(types.String):
def __init__(self, maxlen=None):
super(LengthString, self).__init__()
self.maxlen = maxlen
def __call__(self, value):
value = super(LengthString, self).__call__(value)
if self.maxlen and len(value) > self.maxlen:
raise ValueError(_("String value '%(value)s' exceeds max length "
"%(len)d") % {'value': value,
'len': self.maxlen})
return value
class LengthStrOpt(cfg.Opt):
def __init__(self, name, maxlen=None, **kwargs):
super(LengthStrOpt, self).__init__(name,
type=LengthString(maxlen=maxlen),
**kwargs)
AVAILABILITY_ZONE_OPTS = [
# The default AZ name "nova" is selected to match the default
# AZ name in Nova and Cinder.
LengthStrOpt('availability_zone', maxlen=255, default='nova',
help=_("Availability zone of this node")),
]
def get_log_args(conf, log_file_name, **kwargs):
cmd_args = []
if conf.debug:
@ -128,6 +160,10 @@ def register_process_monitor_opts(conf):
conf.register_opts(PROCESS_MONITOR_OPTS, 'AGENT')
def register_availability_zone_opts_helper(conf):
conf.register_opts(AVAILABILITY_ZONE_OPTS, 'AGENT')
def get_root_helper(conf):
return conf.AGENT.root_helper

View File

@ -548,6 +548,7 @@ class DhcpAgentWithStateReport(DhcpAgent):
self.agent_state = {
'binary': 'neutron-dhcp-agent',
'host': host,
'availability_zone': self.conf.AGENT.availability_zone,
'topic': topics.DHCP_AGENT,
'configurations': {
'dhcp_driver': self.conf.dhcp_driver,

View File

@ -32,6 +32,7 @@ def register_options(conf):
config.register_interface_driver_opts_helper(conf)
config.register_use_namespaces_opts_helper(conf)
config.register_agent_state_opts_helper(conf)
config.register_availability_zone_opts_helper(conf)
conf.register_opts(dhcp_config.DHCP_AGENT_OPTS)
conf.register_opts(dhcp_config.DHCP_OPTS)
conf.register_opts(dhcp_config.DNSMASQ_OPTS)

View File

@ -615,6 +615,7 @@ class L3NATAgentWithStateReport(L3NATAgent):
self.agent_state = {
'binary': 'neutron-l3-agent',
'host': host,
'availability_zone': self.conf.AGENT.availability_zone,
'topic': topics.L3_AGENT,
'configurations': {
'agent_mode': self.conf.agent_mode,

View File

@ -40,6 +40,7 @@ def register_opts(conf):
config.register_agent_state_opts_helper(conf)
conf.register_opts(interface.OPTS)
conf.register_opts(external_process.OPTS)
config.register_availability_zone_opts_helper(conf)
def main(manager='neutron.agent.l3.agent.L3NATAgentWithStateReport'):

View File

@ -20,6 +20,7 @@ from oslo_log import log as logging
import oslo_messaging
from oslo_serialization import jsonutils
from oslo_utils import timeutils
import six
import sqlalchemy as sa
from sqlalchemy.orm import exc
from sqlalchemy import sql
@ -29,6 +30,7 @@ from neutron.common import constants
from neutron.db import model_base
from neutron.db import models_v2
from neutron.extensions import agent as ext_agent
from neutron.extensions import availability_zone as az_ext
from neutron.i18n import _LE, _LI, _LW
from neutron import manager
@ -81,6 +83,7 @@ class Agent(model_base.BASEV2, models_v2.HasId):
topic = sa.Column(sa.String(255), nullable=False)
# TOPIC.host is a target topic
host = sa.Column(sa.String(255), nullable=False)
availability_zone = sa.Column(sa.String(255))
admin_state_up = sa.Column(sa.Boolean, default=True,
server_default=sql.true(), nullable=False)
# the time when first report came from agents
@ -101,7 +104,60 @@ class Agent(model_base.BASEV2, models_v2.HasId):
return not AgentDbMixin.is_agent_down(self.heartbeat_timestamp)
class AgentDbMixin(ext_agent.AgentPluginBase):
class AgentAvailabilityZoneMixin(az_ext.AvailabilityZonePluginBase):
"""Mixin class to add availability_zone extension to AgentDbMixin."""
def _list_availability_zones(self, context, filters=None):
result = {}
query = self._get_collection_query(context, Agent, filters=filters)
for agent in query.group_by(Agent.admin_state_up,
Agent.availability_zone,
Agent.agent_type):
if not agent.availability_zone:
continue
if agent.agent_type == constants.AGENT_TYPE_DHCP:
resource = 'network'
elif agent.agent_type == constants.AGENT_TYPE_L3:
resource = 'router'
else:
continue
key = (agent.availability_zone, resource)
result[key] = agent.admin_state_up or result.get(key, False)
return result
def get_availability_zones(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
"""Return a list of availability zones."""
# NOTE(hichihara): 'tenant_id' is dummy for policy check.
# it is not visible via API.
return [{'state': 'available' if v else 'unavailable',
'name': k[0], 'resource': k[1],
'tenant_id': context.tenant_id}
for k, v in six.iteritems(self._list_availability_zones(
context, filters))]
def validate_availability_zones(self, context, resource_type,
availability_zones):
"""Verify that the availability zones exist."""
if not availability_zones:
return
if resource_type == 'network':
agent_type = constants.AGENT_TYPE_DHCP
elif resource_type == 'router':
agent_type = constants.AGENT_TYPE_L3
else:
return
query = context.session.query(Agent.availability_zone).filter_by(
agent_type=agent_type).group_by(Agent.availability_zone)
query = query.filter(Agent.availability_zone.in_(availability_zones))
azs = [item[0] for item in query]
diff = set(availability_zones) - set(azs)
if diff:
raise az_ext.AvailabilityZoneNotFound(availability_zone=diff.pop())
class AgentDbMixin(ext_agent.AgentPluginBase, AgentAvailabilityZoneMixin):
"""Mixin class to add agent extension to db_base_plugin_v2."""
def _get_agent(self, context, id):
@ -162,6 +218,7 @@ class AgentDbMixin(ext_agent.AgentPluginBase):
res['alive'] = not AgentDbMixin.is_agent_down(
res['heartbeat_timestamp'])
res['configurations'] = self.get_configuration_dict(agent)
res['availability_zone'] = agent['availability_zone']
return self._fields(res, fields)
def delete_agent(self, context, id):
@ -222,7 +279,8 @@ class AgentDbMixin(ext_agent.AgentPluginBase):
with context.session.begin(subtransactions=True):
res_keys = ['agent_type', 'binary', 'host', 'topic']
res = dict((k, agent_state[k]) for k in res_keys)
if 'availability_zone' in agent_state:
res['availability_zone'] = agent_state['availability_zone']
configurations_dict = agent_state.get('configurations', {})
res['configurations'] = jsonutils.dumps(configurations_dict)
res['load'] = self._get_agent_load(agent_state)

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 availability zone
Revision ID: 59cb5b6cf4d
Revises: 34af2b5c5a59
Create Date: 2015-01-20 14:38:47.156574
"""
# revision identifiers, used by Alembic.
revision = '59cb5b6cf4d'
down_revision = '34af2b5c5a59'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('agents',
sa.Column('availability_zone', sa.String(length=255)))

View File

@ -108,6 +108,10 @@ class Agent(extensions.ExtensionDescriptor):
return [ex]
def update_attributes_map(self, attributes):
super(Agent, self).update_attributes_map(
attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP)
def get_extended_resources(self, version):
if version == "2.0":
return RESOURCE_ATTRIBUTE_MAP

View File

@ -0,0 +1,107 @@
#
# 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
from neutron.api import extensions
from neutron.api.v2 import attributes as attr
from neutron.api.v2 import base
from neutron.common import exceptions
from neutron import manager
# Attribute Map
RESOURCE_NAME = 'availability_zone'
AVAILABILITY_ZONES = 'availability_zones'
# name: name of availability zone (string)
# resource: type of resource: 'network' or 'router'
# state: state of availability zone: 'available' or 'unavailable'
# It means whether users can use the availability zone.
RESOURCE_ATTRIBUTE_MAP = {
AVAILABILITY_ZONES: {
'name': {'is_visible': True},
'resource': {'is_visible': True},
'state': {'is_visible': True}
}
}
EXTENDED_ATTRIBUTES_2_0 = {
'agents': {
RESOURCE_NAME: {'allow_post': False, 'allow_put': False,
'is_visible': True}
}
}
class AvailabilityZoneNotFound(exceptions.NotFound):
message = _("AvailabilityZone %(availability_zone)s could not be found.")
class Availability_zone(extensions.ExtensionDescriptor):
"""Availability zone extension."""
@classmethod
def get_name(cls):
return "Availability Zone"
@classmethod
def get_alias(cls):
return "availability_zone"
@classmethod
def get_description(cls):
return "The availability zone extension."
@classmethod
def get_updated(cls):
return "2015-01-01T10:00:00-00:00"
def get_required_extensions(self):
return ["agent"]
@classmethod
def get_resources(cls):
"""Returns Ext Resources."""
my_plurals = [(key, key[:-1]) for key in RESOURCE_ATTRIBUTE_MAP.keys()]
attr.PLURALS.update(dict(my_plurals))
plugin = manager.NeutronManager.get_plugin()
params = RESOURCE_ATTRIBUTE_MAP.get(AVAILABILITY_ZONES)
controller = base.create_resource(AVAILABILITY_ZONES,
RESOURCE_NAME, plugin, params)
ex = extensions.ResourceExtension(AVAILABILITY_ZONES, controller)
return [ex]
def get_extended_resources(self, version):
if version == "2.0":
return dict(list(EXTENDED_ATTRIBUTES_2_0.items()) +
list(RESOURCE_ATTRIBUTE_MAP.items()))
else:
return {}
class AvailabilityZonePluginBase(object):
"""REST API to operate the Availability Zone."""
@abc.abstractmethod
def get_availability_zones(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
pass
@abc.abstractmethod
def validate_availability_zones(self, context, resource_type,
availability_zones):
pass

View File

@ -118,7 +118,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
"multi-provider", "allowed-address-pairs",
"extra_dhcp_opt", "subnet_allocation",
"net-mtu", "vlan-transparent",
"address-scope", "dns-integration"]
"address-scope", "dns-integration",
"availability_zone"]
@property
def supported_extension_aliases(self):

View File

@ -25,6 +25,7 @@ from neutron.db import agents_db
from neutron.db import common_db_mixin
HOST = 'localhost'
DEFAULT_AZ = 'nova'
def find_file(filename, path):
@ -47,12 +48,14 @@ class FakePlugin(common_db_mixin.CommonDbMixin,
def _get_l3_agent_dict(host, agent_mode, internal_only=True,
ext_net_id='', ext_bridge='', router_id=None):
ext_net_id='', ext_bridge='', router_id=None,
az=DEFAULT_AZ):
return {
'agent_type': constants.AGENT_TYPE_L3,
'binary': 'neutron-l3-agent',
'host': host,
'topic': topics.L3_AGENT,
'availability_zone': az,
'configurations': {'agent_mode': agent_mode,
'handle_internal_only_routers': internal_only,
'external_network_bridge': ext_bridge,
@ -71,18 +74,19 @@ def _register_agent(agent):
def register_l3_agent(host=HOST, agent_mode=constants.L3_AGENT_MODE_LEGACY,
internal_only=True, ext_net_id='', ext_bridge='',
router_id=None):
router_id=None, az=DEFAULT_AZ):
agent = _get_l3_agent_dict(host, agent_mode, internal_only, ext_net_id,
ext_bridge, router_id)
ext_bridge, router_id, az)
return _register_agent(agent)
def _get_dhcp_agent_dict(host, networks=0):
def _get_dhcp_agent_dict(host, networks=0, az=DEFAULT_AZ):
agent = {
'binary': 'neutron-dhcp-agent',
'host': host,
'topic': topics.DHCP_AGENT,
'agent_type': constants.AGENT_TYPE_DHCP,
'availability_zone': az,
'configurations': {'dhcp_driver': 'dhcp_driver',
'use_namespaces': True,
'networks': networks}}
@ -90,9 +94,9 @@ def _get_dhcp_agent_dict(host, networks=0):
def register_dhcp_agent(host=HOST, networks=0, admin_state_up=True,
alive=True):
alive=True, az=DEFAULT_AZ):
agent = _register_agent(
_get_dhcp_agent_dict(host, networks))
_get_dhcp_agent_dict(host, networks, az=az))
if not admin_state_up:
set_agent_admin_state(agent['id'])

View File

@ -70,6 +70,7 @@ class BasicRouterOperationsFramework(base.BaseTestCase):
agent_config.register_interface_driver_opts_helper(self.conf)
agent_config.register_use_namespaces_opts_helper(self.conf)
agent_config.register_process_monitor_opts(self.conf)
agent_config.register_availability_zone_opts_helper(self.conf)
self.conf.register_opts(interface.OPTS)
self.conf.register_opts(external_process.OPTS)
self.conf.set_override('router_id', 'fake_id')

View File

@ -0,0 +1,98 @@
#
# 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 import context
from neutron.db import agents_db
from neutron.db import db_base_plugin_v2
from neutron.extensions import agent
from neutron.extensions import availability_zone as az_ext
from neutron.tests.common import helpers
from neutron.tests.unit.db import test_db_base_plugin_v2
LOG = logging.getLogger(__name__)
class AZExtensionManager(object):
def get_resources(self):
agent.RESOURCE_ATTRIBUTE_MAP['agents'].update(
az_ext.EXTENDED_ATTRIBUTES_2_0['agents'])
return (az_ext.Availability_zone.get_resources() +
agent.Agent.get_resources())
def get_actions(self):
return []
def get_request_extensions(self):
return []
# This plugin class is just for testing
class AZTestPlugin(db_base_plugin_v2.NeutronDbPluginV2,
agents_db.AgentDbMixin):
supported_extension_aliases = ["agent", "availability_zone"]
class AZTestCommon(test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
def _register_azs(self):
self.agent1 = helpers.register_dhcp_agent(host='host1', az='nova1')
self.agent2 = helpers.register_dhcp_agent(host='host2', az='nova2')
self.agent3 = helpers.register_l3_agent(host='host2', az='nova2')
self.agent4 = helpers.register_l3_agent(host='host3', az='nova3')
self.agent5 = helpers.register_l3_agent(host='host4', az='nova2')
class TestAZAgentCase(AZTestCommon):
def setUp(self):
plugin = ('neutron.tests.unit.extensions.'
'test_availability_zone.AZTestPlugin')
ext_mgr = AZExtensionManager()
super(TestAZAgentCase, self).setUp(plugin=plugin, ext_mgr=ext_mgr)
def test_list_availability_zones(self):
self._register_azs()
helpers.set_agent_admin_state(self.agent3['id'], admin_state_up=False)
helpers.set_agent_admin_state(self.agent4['id'], admin_state_up=False)
expected = [
{'name': 'nova1', 'resource': 'network', 'state': 'available'},
{'name': 'nova2', 'resource': 'network', 'state': 'available'},
{'name': 'nova2', 'resource': 'router', 'state': 'available'},
{'name': 'nova3', 'resource': 'router', 'state': 'unavailable'}]
res = self._list('availability_zones')
azs = res['availability_zones']
self.assertItemsEqual(expected, azs)
# not admin case
ctx = context.Context('', 'noadmin')
res = self._list('availability_zones', neutron_context=ctx)
azs = res['availability_zones']
self.assertItemsEqual(expected, azs)
def test_list_agent_with_az(self):
helpers.register_dhcp_agent(host='host1', az='nova1')
res = self._list('agents')
self.assertEqual('nova1',
res['agents'][0]['availability_zone'])
def test_validate_availability_zones(self):
self._register_azs()
ctx = context.Context('', 'tenant_id')
self.plugin.validate_availability_zones(ctx, 'network',
['nova1', 'nova2'])
self.plugin.validate_availability_zones(ctx, 'router',
['nova2', 'nova3'])
self.assertRaises(az_ext.AvailabilityZoneNotFound,
self.plugin.validate_availability_zones,
ctx, 'router', ['nova1'])