# 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
import os
from oslo_config import cfg
+from oslo_config import types
from oslo_log import log as logging
from neutron.common import config
]
+# 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:
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
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,
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)
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,
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'):
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
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
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
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):
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):
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)
--- /dev/null
+#
+# 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)))
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
--- /dev/null
+#
+# 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
"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):
from neutron.db import common_db_mixin
HOST = 'localhost'
+DEFAULT_AZ = 'nova'
def find_file(filename, path):
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,
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}}
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'])
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')
--- /dev/null
+#
+# 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'])