Notify ironic on port status changes
This patch adds an ironic notifier that sends notifications to ironic endpoint /v1/events. The events are triggered by port updates and deletions. Only ports with vnic_type baremetal are honored. Story: 1304673 Task: 22263 Closes-Bug: #1828367 Implements: blueprint event-notifier-ironic Authored-By: Vasyl Saienko <vsaienko@mirantis.com> Co-Authored-By: Harald Jensås <hjensas@redhat.com> Co-Authored-By: Julia Kreger <juliaashleykreger@gmail.com> Change-Id: I0bb3187a88a7f20adb8c60e24945db159afb83f1
This commit is contained in:
parent
3f837836f6
commit
afff649a39
@ -7,6 +7,7 @@ namespace = neutron.agent
|
|||||||
namespace = neutron.db
|
namespace = neutron.db
|
||||||
namespace = neutron.extensions
|
namespace = neutron.extensions
|
||||||
namespace = nova.auth
|
namespace = nova.auth
|
||||||
|
namespace = ironic.auth
|
||||||
namespace = oslo.log
|
namespace = oslo.log
|
||||||
namespace = oslo.db
|
namespace = oslo.db
|
||||||
namespace = oslo.policy
|
namespace = oslo.policy
|
||||||
|
@ -110,6 +110,7 @@ pyroute2==0.5.3
|
|||||||
python-dateutil==2.5.3
|
python-dateutil==2.5.3
|
||||||
python-designateclient==2.7.0
|
python-designateclient==2.7.0
|
||||||
python-editor==1.0.3
|
python-editor==1.0.3
|
||||||
|
python-ironicclient==2.7.0
|
||||||
python-keystoneclient==3.8.0
|
python-keystoneclient==3.8.0
|
||||||
python-mimeparse==1.6.0
|
python-mimeparse==1.6.0
|
||||||
python-neutronclient==6.7.0
|
python-neutronclient==6.7.0
|
||||||
|
@ -72,6 +72,13 @@ common_config.register_placement_opts()
|
|||||||
|
|
||||||
logging.register_options(cfg.CONF)
|
logging.register_options(cfg.CONF)
|
||||||
|
|
||||||
|
# Register the ironic configuration options
|
||||||
|
ks_loading.register_auth_conf_options(cfg.CONF,
|
||||||
|
common_config.IRONIC_CONF_SECTION)
|
||||||
|
ks_loading.register_session_conf_options(cfg.CONF,
|
||||||
|
common_config.IRONIC_CONF_SECTION)
|
||||||
|
common_config.register_ironic_opts()
|
||||||
|
|
||||||
|
|
||||||
def init(args, default_config_files=None, **kwargs):
|
def init(args, default_config_files=None, **kwargs):
|
||||||
cfg.CONF(args=args, project='neutron',
|
cfg.CONF(args=args, project='neutron',
|
||||||
|
@ -190,3 +190,43 @@ placement_opts = [
|
|||||||
|
|
||||||
def register_placement_opts(cfg=cfg.CONF):
|
def register_placement_opts(cfg=cfg.CONF):
|
||||||
cfg.register_opts(placement_opts, group=PLACEMENT_CONF_SECTION)
|
cfg.register_opts(placement_opts, group=PLACEMENT_CONF_SECTION)
|
||||||
|
|
||||||
|
|
||||||
|
IRONIC_CONF_SECTION = 'ironic'
|
||||||
|
|
||||||
|
ironic_opts = [
|
||||||
|
cfg.BoolOpt('enable_notifications', default=False,
|
||||||
|
help=_("Send notification events to ironic. (For example on "
|
||||||
|
"relevant port status changes.)")),
|
||||||
|
cfg.StrOpt('region_name',
|
||||||
|
help=_('Name of region used to get Ironic endpoints. Useful if'
|
||||||
|
'keystone manages more than one region.')),
|
||||||
|
cfg.StrOpt('endpoint_type',
|
||||||
|
default='public',
|
||||||
|
choices=['public', 'admin', 'internal'],
|
||||||
|
help=_('Type of the ironic endpoint to use. This endpoint '
|
||||||
|
'will be looked up in the keystone catalog and should '
|
||||||
|
'be one of public, internal or admin.')),
|
||||||
|
cfg.StrOpt('auth_strategy',
|
||||||
|
default='keystone',
|
||||||
|
choices=('keystone', 'noauth'),
|
||||||
|
help=_('Method to use for authentication: noauth or '
|
||||||
|
'keystone.')),
|
||||||
|
cfg.StrOpt('ironic_url',
|
||||||
|
default='http://localhost:6385/',
|
||||||
|
help=_('Ironic API URL, used to set Ironic API URL when '
|
||||||
|
'auth_strategy option is noauth to work with standalone '
|
||||||
|
'Ironic without keystone.')),
|
||||||
|
cfg.IntOpt('retry_interval',
|
||||||
|
default=2,
|
||||||
|
help=_('Interval between retries in case of conflict error '
|
||||||
|
'(HTTP 409).')),
|
||||||
|
cfg.IntOpt('max_retries',
|
||||||
|
default=30,
|
||||||
|
help=_('Maximum number of retries in case of conflict error '
|
||||||
|
'(HTTP 409).')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def register_ironic_opts(cfg=cfg.CONF):
|
||||||
|
cfg.register_opts(ironic_opts, group=IRONIC_CONF_SECTION)
|
||||||
|
@ -171,6 +171,10 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||||||
db_api.sqla_listen(
|
db_api.sqla_listen(
|
||||||
models_v2.Port.status, 'set',
|
models_v2.Port.status, 'set',
|
||||||
self.nova_notifier.record_port_status_changed)
|
self.nova_notifier.record_port_status_changed)
|
||||||
|
if cfg.CONF.ironic.enable_notifications:
|
||||||
|
# Import ironic notifier conditionally
|
||||||
|
from neutron.notifiers import ironic
|
||||||
|
self.ironic_notifier = ironic.Notifier.get_instance()
|
||||||
|
|
||||||
@registry.receives(resources.RBAC_POLICY, [events.BEFORE_CREATE,
|
@registry.receives(resources.RBAC_POLICY, [events.BEFORE_CREATE,
|
||||||
events.BEFORE_UPDATE,
|
events.BEFORE_UPDATE,
|
||||||
|
159
neutron/notifiers/ironic.py
Normal file
159
neutron/notifiers/ironic.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# Copyright (c) 2019 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.
|
||||||
|
|
||||||
|
from ironicclient import client
|
||||||
|
from ironicclient import exc as ironic_exc
|
||||||
|
from keystoneauth1 import loading as ks_loading
|
||||||
|
from neutron_lib.api.definitions import port as port_def
|
||||||
|
from neutron_lib.api.definitions import portbindings as portbindings_def
|
||||||
|
from neutron_lib.callbacks import events
|
||||||
|
from neutron_lib.callbacks import registry
|
||||||
|
from neutron_lib.callbacks import resources
|
||||||
|
from neutron_lib import constants as n_const
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from neutron.notifiers import batch_notifier
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BAREMETAL_EVENT_TYPE = 'network'
|
||||||
|
IRONIC_API_VERSION = 'latest'
|
||||||
|
IRONIC_SESSION = None
|
||||||
|
IRONIC_CONF_SECTION = 'ironic'
|
||||||
|
IRONIC_CLIENT_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
@registry.has_registry_receivers
|
||||||
|
class Notifier(object):
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_instance(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = cls()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.batch_notifier = batch_notifier.BatchNotifier(
|
||||||
|
cfg.CONF.send_events_interval, self.send_events)
|
||||||
|
self.irclient = self._get_ironic_client()
|
||||||
|
|
||||||
|
def _get_session(self, group):
|
||||||
|
auth = ks_loading.load_auth_from_conf_options(cfg.CONF, group)
|
||||||
|
session = ks_loading.load_session_from_conf_options(
|
||||||
|
cfg.CONF, group, auth=auth)
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _get_ironic_client(self):
|
||||||
|
"""Get Ironic client instance."""
|
||||||
|
|
||||||
|
# NOTE: To support standalone ironic without keystone
|
||||||
|
if cfg.CONF.ironic.auth_strategy == 'noauth':
|
||||||
|
args = {'token': 'noauth',
|
||||||
|
'endpoint': cfg.CONF.ironic.ironic_url}
|
||||||
|
else:
|
||||||
|
global IRONIC_SESSION
|
||||||
|
if not IRONIC_SESSION:
|
||||||
|
IRONIC_SESSION = self._get_session(IRONIC_CONF_SECTION)
|
||||||
|
args = {'session': IRONIC_SESSION,
|
||||||
|
'region_name': cfg.CONF.ironic.region_name,
|
||||||
|
'endpoint_type': cfg.CONF.ironic.endpoint_type}
|
||||||
|
args['os_ironic_api_version'] = IRONIC_API_VERSION
|
||||||
|
args['max_retries'] = cfg.CONF.ironic.max_retries
|
||||||
|
args['retry_interval'] = cfg.CONF.ironic.retry_interval
|
||||||
|
return client.Client(IRONIC_CLIENT_VERSION, **args)
|
||||||
|
|
||||||
|
def send_events(self, batched_events):
|
||||||
|
# NOTE(TheJulia): Friendly exception handling so operators
|
||||||
|
# can decouple updates.
|
||||||
|
try:
|
||||||
|
self.irclient.events.create(events=batched_events)
|
||||||
|
except ironic_exc.NotFound:
|
||||||
|
LOG.error('The ironic API appears to not support posting events. '
|
||||||
|
'The API likely needs to be upgraded.')
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Unknown error encountered posting the event to '
|
||||||
|
'ironic. {error}'.format(error=e))
|
||||||
|
|
||||||
|
@registry.receives(resources.PORT, [events.AFTER_UPDATE])
|
||||||
|
def process_port_update_event(self, resource, event, trigger,
|
||||||
|
original_port=None, port=None,
|
||||||
|
**kwargs):
|
||||||
|
# We only want to notify about baremetal ports.
|
||||||
|
if not (port[portbindings_def.VNIC_TYPE] ==
|
||||||
|
portbindings_def.VNIC_BAREMETAL):
|
||||||
|
# TODO(TheJulia): Add the smartnic flag at some point...
|
||||||
|
return
|
||||||
|
|
||||||
|
original_port_status = original_port['status']
|
||||||
|
current_port_status = port['status']
|
||||||
|
port_event = None
|
||||||
|
if (original_port_status == n_const.PORT_STATUS_ACTIVE and
|
||||||
|
current_port_status in [n_const.PORT_STATUS_DOWN,
|
||||||
|
n_const.PORT_STATUS_ERROR]):
|
||||||
|
port_event = 'unbind_port'
|
||||||
|
elif (original_port_status == n_const.PORT_STATUS_DOWN and
|
||||||
|
current_port_status in [n_const.PORT_STATUS_ACTIVE,
|
||||||
|
n_const.PORT_STATUS_ERROR]):
|
||||||
|
port_event = 'bind_port'
|
||||||
|
LOG.debug('Queuing event for {event_type} for port {port} '
|
||||||
|
'for status {status}.'.format(event_type=port_event,
|
||||||
|
port=port['id'],
|
||||||
|
status=current_port_status))
|
||||||
|
if port_event:
|
||||||
|
notify_event = {
|
||||||
|
'event': '.'.join([BAREMETAL_EVENT_TYPE, port_event]),
|
||||||
|
'port_id': port['id'],
|
||||||
|
'mac_address': port[port_def.PORT_MAC_ADDRESS],
|
||||||
|
'status': current_port_status,
|
||||||
|
'device_id': port['device_id'],
|
||||||
|
'binding:host_id': port[portbindings_def.HOST_ID],
|
||||||
|
'binding:vnic_type': port[portbindings_def.VNIC_TYPE]
|
||||||
|
}
|
||||||
|
# Filter keys with empty string as value. In case a type UUID field
|
||||||
|
# or similar is not set the API won't accept empty string.
|
||||||
|
self.batch_notifier.queue_event(
|
||||||
|
{k: v for k, v in notify_event.items() if v != ''})
|
||||||
|
|
||||||
|
@registry.receives(resources.PORT, [events.AFTER_DELETE])
|
||||||
|
def process_port_delete_event(self, resource, event, trigger,
|
||||||
|
original_port=None, port=None,
|
||||||
|
**kwargs):
|
||||||
|
# We only want to notify about baremetal ports.
|
||||||
|
if not (port[portbindings_def.VNIC_TYPE] ==
|
||||||
|
portbindings_def.VNIC_BAREMETAL):
|
||||||
|
# TODO(TheJulia): Add the smartnic flag at some point...
|
||||||
|
return
|
||||||
|
|
||||||
|
port_event = 'delete_port'
|
||||||
|
LOG.debug('Queuing event for {event_type} for port {port} '
|
||||||
|
'for status {status}.'.format(event_type=port_event,
|
||||||
|
port=port['id'],
|
||||||
|
status='DELETED'))
|
||||||
|
notify_event = {
|
||||||
|
'event': '.'.join([BAREMETAL_EVENT_TYPE, port_event]),
|
||||||
|
'port_id': port['id'],
|
||||||
|
'mac_address': port[port_def.PORT_MAC_ADDRESS],
|
||||||
|
'status': 'DELETED',
|
||||||
|
'device_id': port['device_id'],
|
||||||
|
'binding:host_id': port[portbindings_def.HOST_ID],
|
||||||
|
'binding:vnic_type': port[portbindings_def.VNIC_TYPE]
|
||||||
|
}
|
||||||
|
# Filter keys with empty string as value. In case a type UUID field
|
||||||
|
# or similar is not set the API won't accept empty string.
|
||||||
|
self.batch_notifier.queue_event(
|
||||||
|
{k: v for k, v in notify_event.items() if v != ''})
|
@ -63,6 +63,7 @@ import neutron.wsgi
|
|||||||
|
|
||||||
|
|
||||||
NOVA_GROUP = 'nova'
|
NOVA_GROUP = 'nova'
|
||||||
|
IRONIC_GROUP = 'ironic'
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
@ -75,6 +76,8 @@ deprecations = {'nova.cafile': [cfg.DeprecatedOpt('ca_certificates_file',
|
|||||||
|
|
||||||
_nova_options = ks_loading.register_session_conf_options(
|
_nova_options = ks_loading.register_session_conf_options(
|
||||||
CONF, NOVA_GROUP, deprecated_opts=deprecations)
|
CONF, NOVA_GROUP, deprecated_opts=deprecations)
|
||||||
|
_ironic_options = ks_loading.register_session_conf_options(
|
||||||
|
CONF, IRONIC_GROUP)
|
||||||
|
|
||||||
|
|
||||||
def list_agent_opts():
|
def list_agent_opts():
|
||||||
@ -141,6 +144,10 @@ def list_opts():
|
|||||||
itertools.chain(
|
itertools.chain(
|
||||||
neutron.conf.common.nova_opts)
|
neutron.conf.common.nova_opts)
|
||||||
),
|
),
|
||||||
|
(neutron.conf.common.IRONIC_CONF_SECTION,
|
||||||
|
itertools.chain(
|
||||||
|
neutron.conf.common.ironic_opts)
|
||||||
|
),
|
||||||
('quotas', neutron.conf.quota.core_quota_opts)
|
('quotas', neutron.conf.quota.core_quota_opts)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -318,6 +325,20 @@ def list_auth_opts():
|
|||||||
return [(NOVA_GROUP, opt_list)]
|
return [(NOVA_GROUP, opt_list)]
|
||||||
|
|
||||||
|
|
||||||
|
def list_ironic_auth_opts():
|
||||||
|
opt_list = copy.deepcopy(_ironic_options)
|
||||||
|
opt_list.insert(0, ks_loading.get_auth_common_conf_options()[0])
|
||||||
|
# NOTE(mhickey): There are a lot of auth plugins, we just generate
|
||||||
|
# the config options for a few common ones
|
||||||
|
plugins = ['password', 'v2password', 'v3password']
|
||||||
|
for name in plugins:
|
||||||
|
for plugin_option in ks_loading.get_auth_plugin_conf_options(name):
|
||||||
|
if all(option.name != plugin_option.name for option in opt_list):
|
||||||
|
opt_list.append(plugin_option)
|
||||||
|
opt_list.sort(key=operator.attrgetter('name'))
|
||||||
|
return [(IRONIC_GROUP, opt_list)]
|
||||||
|
|
||||||
|
|
||||||
def list_xenapi_opts():
|
def list_xenapi_opts():
|
||||||
return [
|
return [
|
||||||
('xenapi',
|
('xenapi',
|
||||||
|
202
neutron/tests/unit/notifiers/test_ironic.py
Normal file
202
neutron/tests/unit/notifiers/test_ironic.py
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
# Copyright (c) 2019 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 eventlet
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from ironicclient import client
|
||||||
|
from ironicclient import exc as ironic_exc
|
||||||
|
from neutron_lib.api.definitions import portbindings as portbindings_def
|
||||||
|
from neutron_lib import constants as n_const
|
||||||
|
|
||||||
|
from neutron.notifiers import batch_notifier
|
||||||
|
from neutron.notifiers import ironic
|
||||||
|
from neutron.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
DEVICE_OWNER_BAREMETAL = n_const.DEVICE_OWNER_BAREMETAL_PREFIX + 'fake'
|
||||||
|
|
||||||
|
|
||||||
|
def get_fake_port():
|
||||||
|
return {'id': '11111111-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'binding:host_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'binding:vnic_type': portbindings_def.VNIC_BAREMETAL,
|
||||||
|
'device_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'mac_address': 'de:ad:ca:fe:ba:be'}
|
||||||
|
|
||||||
|
|
||||||
|
class TestIronicNotifier(base.BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestIronicNotifier, self).setUp()
|
||||||
|
self.ironic_notifier = ironic.Notifier()
|
||||||
|
|
||||||
|
@mock.patch.object(batch_notifier.BatchNotifier, 'queue_event',
|
||||||
|
autospec=True)
|
||||||
|
def test_process_port_update_event_bind_port(self, mock_queue_event):
|
||||||
|
port = get_fake_port()
|
||||||
|
port.update({'status': n_const.PORT_STATUS_ACTIVE})
|
||||||
|
original_port = get_fake_port()
|
||||||
|
original_port.update({'status': n_const.PORT_STATUS_DOWN})
|
||||||
|
self.ironic_notifier.process_port_update_event(
|
||||||
|
'fake_resource', 'fake_event', 'fake_trigger',
|
||||||
|
original_port=original_port, port=port, **{})
|
||||||
|
mock_queue_event.assert_called_with(
|
||||||
|
self.ironic_notifier.batch_notifier,
|
||||||
|
{'event': 'network.bind_port',
|
||||||
|
'binding:host_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'binding:vnic_type': portbindings_def.VNIC_BAREMETAL,
|
||||||
|
'device_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'port_id': '11111111-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'mac_address': 'de:ad:ca:fe:ba:be',
|
||||||
|
'status': n_const.PORT_STATUS_ACTIVE})
|
||||||
|
|
||||||
|
@mock.patch.object(batch_notifier.BatchNotifier, 'queue_event',
|
||||||
|
autospec=True)
|
||||||
|
def test_process_port_update_event_bind_port_err(self, mock_queue_event):
|
||||||
|
port = get_fake_port()
|
||||||
|
port.update({'status': n_const.PORT_STATUS_ERROR})
|
||||||
|
original_port = get_fake_port()
|
||||||
|
original_port.update({'status': n_const.PORT_STATUS_DOWN})
|
||||||
|
self.ironic_notifier.process_port_update_event(
|
||||||
|
'fake_resource', 'fake_event', 'fake_trigger',
|
||||||
|
original_port=original_port, port=port, **{})
|
||||||
|
mock_queue_event.assert_called_with(
|
||||||
|
self.ironic_notifier.batch_notifier,
|
||||||
|
{'event': 'network.bind_port',
|
||||||
|
'binding:host_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'binding:vnic_type': portbindings_def.VNIC_BAREMETAL,
|
||||||
|
'device_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'port_id': '11111111-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'mac_address': 'de:ad:ca:fe:ba:be',
|
||||||
|
'status': n_const.PORT_STATUS_ERROR})
|
||||||
|
|
||||||
|
@mock.patch.object(batch_notifier.BatchNotifier, 'queue_event',
|
||||||
|
autospec=True)
|
||||||
|
def test_process_port_update_event_unbind_port(self, mock_queue_event):
|
||||||
|
port = get_fake_port()
|
||||||
|
port.update({'status': n_const.PORT_STATUS_DOWN})
|
||||||
|
original_port = get_fake_port()
|
||||||
|
original_port.update({'status': n_const.PORT_STATUS_ACTIVE})
|
||||||
|
self.ironic_notifier.process_port_update_event(
|
||||||
|
'fake_resource', 'fake_event', 'fake_trigger',
|
||||||
|
original_port=original_port, port=port, **{})
|
||||||
|
mock_queue_event.assert_called_with(
|
||||||
|
self.ironic_notifier.batch_notifier,
|
||||||
|
{'event': 'network.unbind_port',
|
||||||
|
'binding:host_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'binding:vnic_type': portbindings_def.VNIC_BAREMETAL,
|
||||||
|
'device_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'port_id': '11111111-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'mac_address': 'de:ad:ca:fe:ba:be',
|
||||||
|
'status': n_const.PORT_STATUS_DOWN})
|
||||||
|
|
||||||
|
@mock.patch.object(batch_notifier.BatchNotifier, 'queue_event',
|
||||||
|
autospec=True)
|
||||||
|
def test_process_port_update_event_unbind_port_err(self, mock_queue_event):
|
||||||
|
port = get_fake_port()
|
||||||
|
port.update({'status': n_const.PORT_STATUS_ERROR})
|
||||||
|
original_port = get_fake_port()
|
||||||
|
original_port.update({'status': n_const.PORT_STATUS_ACTIVE})
|
||||||
|
self.ironic_notifier.process_port_update_event(
|
||||||
|
'fake_resource', 'fake_event', 'fake_trigger',
|
||||||
|
original_port=original_port, port=port, **{})
|
||||||
|
mock_queue_event.assert_called_with(
|
||||||
|
self.ironic_notifier.batch_notifier,
|
||||||
|
{'event': 'network.unbind_port',
|
||||||
|
'binding:host_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'binding:vnic_type': portbindings_def.VNIC_BAREMETAL,
|
||||||
|
'device_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'port_id': '11111111-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'mac_address': 'de:ad:ca:fe:ba:be',
|
||||||
|
'status': n_const.PORT_STATUS_ERROR})
|
||||||
|
|
||||||
|
@mock.patch.object(batch_notifier.BatchNotifier, 'queue_event',
|
||||||
|
autospec=True)
|
||||||
|
def test_process_port_delete_event(self, mock_queue_event):
|
||||||
|
port = get_fake_port()
|
||||||
|
self.ironic_notifier.process_port_delete_event(
|
||||||
|
'fake_resource', 'fake_event', 'fake_trigger', original_port=None,
|
||||||
|
port=port, **{})
|
||||||
|
mock_queue_event.assert_called_with(
|
||||||
|
self.ironic_notifier.batch_notifier,
|
||||||
|
{'event': 'network.delete_port',
|
||||||
|
'binding:host_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'binding:vnic_type': portbindings_def.VNIC_BAREMETAL,
|
||||||
|
'device_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'port_id': '11111111-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'mac_address': 'de:ad:ca:fe:ba:be',
|
||||||
|
'status': 'DELETED'})
|
||||||
|
|
||||||
|
@mock.patch.object(batch_notifier.BatchNotifier, 'queue_event',
|
||||||
|
autospec=True)
|
||||||
|
def test_process_port_event_empty_uuid_field(self, mock_queue_event):
|
||||||
|
port = get_fake_port()
|
||||||
|
port.update({'device_id': ''})
|
||||||
|
self.ironic_notifier.process_port_delete_event(
|
||||||
|
'fake_resource', 'fake_event', 'fake_trigger', original_port=None,
|
||||||
|
port=port, **{})
|
||||||
|
mock_queue_event.assert_called_with(
|
||||||
|
self.ironic_notifier.batch_notifier,
|
||||||
|
{'event': 'network.delete_port',
|
||||||
|
'binding:host_id': '22222222-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'binding:vnic_type': portbindings_def.VNIC_BAREMETAL,
|
||||||
|
'port_id': '11111111-aaaa-bbbb-cccc-555555555555',
|
||||||
|
'mac_address': 'de:ad:ca:fe:ba:be',
|
||||||
|
'status': 'DELETED'})
|
||||||
|
|
||||||
|
@mock.patch.object(eventlet, 'spawn_n', autospec=True)
|
||||||
|
def test_queue_events(self, mock_spawn_n):
|
||||||
|
port = get_fake_port()
|
||||||
|
self.ironic_notifier.process_port_delete_event(
|
||||||
|
'fake_resource', 'fake_event', 'fake_trigger', original_port=None,
|
||||||
|
port=port, **{})
|
||||||
|
|
||||||
|
port = get_fake_port()
|
||||||
|
port.update({'status': n_const.PORT_STATUS_ACTIVE})
|
||||||
|
original_port = get_fake_port()
|
||||||
|
original_port.update({'status': n_const.PORT_STATUS_DOWN})
|
||||||
|
self.ironic_notifier.process_port_update_event(
|
||||||
|
'fake_resource', 'fake_event', 'fake_trigger',
|
||||||
|
original_port=original_port, port=port, **{})
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
2, len(self.ironic_notifier.batch_notifier.pending_events))
|
||||||
|
self.assertEqual(2, mock_spawn_n.call_count)
|
||||||
|
|
||||||
|
@mock.patch.object(client, 'Client', autospec=False)
|
||||||
|
def test_send_events(self, mock_client):
|
||||||
|
self.ironic_notifier.irclient = mock_client
|
||||||
|
self.ironic_notifier.send_events(['test', 'events'])
|
||||||
|
mock_client.events.create.assert_called_with(events=['test', 'events'])
|
||||||
|
|
||||||
|
@mock.patch.object(ironic.LOG, 'error', autospec=True)
|
||||||
|
@mock.patch.object(client, 'Client', autospec=False)
|
||||||
|
def test_send_event_method_not_found(self, mock_client, mock_log):
|
||||||
|
self.ironic_notifier.irclient = mock_client
|
||||||
|
exception = ironic_exc.NotFound()
|
||||||
|
mock_client.events.create.side_effect = exception
|
||||||
|
self.ironic_notifier.send_events(['test', 'events'])
|
||||||
|
self.assertEqual(1, mock_log.call_count)
|
||||||
|
mock_log.assert_called_with('The ironic API appears to not support '
|
||||||
|
'posting events. The API likely needs to '
|
||||||
|
'be upgraded.')
|
||||||
|
|
||||||
|
@mock.patch.object(ironic.LOG, 'error', autospec=True)
|
||||||
|
@mock.patch.object(client, 'Client', autospec=False)
|
||||||
|
def test_send_event_exception(self, mock_client, mock_log):
|
||||||
|
self.ironic_notifier.irclient = mock_client
|
||||||
|
mock_client.events.create.side_effect = Exception()
|
||||||
|
self.ironic_notifier.send_events(['test', 'events'])
|
||||||
|
self.assertEqual(1, mock_log.call_count)
|
9
releasenotes/notes/notifier-ironic-66391e083d78fee2.yaml
Normal file
9
releasenotes/notes/notifier-ironic-66391e083d78fee2.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
A notifier for the Openstack Baremetal service (``ironic``) is introduced.
|
||||||
|
When enabled notifications are sent to the Baremetal service on relevant
|
||||||
|
resource events/changes. By default notifications to the Baremetal service
|
||||||
|
is *disabled*. To *enable* notifications to the Baremetal service set
|
||||||
|
``[ironic]/enable_notifications`` to ``True`` in the Networking service
|
||||||
|
configuration (``neutron.conf``).
|
@ -51,5 +51,6 @@ pyroute2>=0.5.3;sys_platform!='win32' # Apache-2.0 (+ dual licensed GPL2)
|
|||||||
weakrefmethod>=1.0.2;python_version=='2.7' # PSF
|
weakrefmethod>=1.0.2;python_version=='2.7' # PSF
|
||||||
|
|
||||||
python-novaclient>=9.1.0 # Apache-2.0
|
python-novaclient>=9.1.0 # Apache-2.0
|
||||||
|
python-ironicclient>=2.7.0 # Apache-2.0
|
||||||
python-designateclient>=2.7.0 # Apache-2.0
|
python-designateclient>=2.7.0 # Apache-2.0
|
||||||
os-xenapi>=0.3.1 # Apache-2.0
|
os-xenapi>=0.3.1 # Apache-2.0
|
||||||
|
@ -128,6 +128,7 @@ neutron.agent.linux.pd_drivers =
|
|||||||
neutron.services.external_dns_drivers =
|
neutron.services.external_dns_drivers =
|
||||||
designate = neutron.services.externaldns.drivers.designate.driver:Designate
|
designate = neutron.services.externaldns.drivers.designate.driver:Designate
|
||||||
oslo.config.opts =
|
oslo.config.opts =
|
||||||
|
ironic.auth = neutron.opts:list_ironic_auth_opts
|
||||||
neutron = neutron.opts:list_opts
|
neutron = neutron.opts:list_opts
|
||||||
neutron.agent = neutron.opts:list_agent_opts
|
neutron.agent = neutron.opts:list_agent_opts
|
||||||
neutron.az.agent = neutron.opts:list_az_agent_opts
|
neutron.az.agent = neutron.opts:list_az_agent_opts
|
||||||
|
Loading…
Reference in New Issue
Block a user