Merge "Notify ironic on port status changes"
This commit is contained in:
commit
0e2508c8b1
@ -7,6 +7,7 @@ namespace = neutron.agent
|
||||
namespace = neutron.db
|
||||
namespace = neutron.extensions
|
||||
namespace = nova.auth
|
||||
namespace = ironic.auth
|
||||
namespace = oslo.log
|
||||
namespace = oslo.db
|
||||
namespace = oslo.policy
|
||||
|
@ -111,6 +111,7 @@ pyroute2==0.5.3
|
||||
python-dateutil==2.5.3
|
||||
python-designateclient==2.7.0
|
||||
python-editor==1.0.3
|
||||
python-ironicclient==2.7.0
|
||||
python-keystoneclient==3.8.0
|
||||
python-mimeparse==1.6.0
|
||||
python-neutronclient==6.7.0
|
||||
|
@ -72,6 +72,13 @@ common_config.register_placement_opts()
|
||||
|
||||
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):
|
||||
cfg.CONF(args=args, project='neutron',
|
||||
|
@ -190,3 +190,43 @@ placement_opts = [
|
||||
|
||||
def register_placement_opts(cfg=cfg.CONF):
|
||||
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(
|
||||
models_v2.Port.status, 'set',
|
||||
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,
|
||||
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'
|
||||
IRONIC_GROUP = 'ironic'
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
@ -75,6 +76,8 @@ deprecations = {'nova.cafile': [cfg.DeprecatedOpt('ca_certificates_file',
|
||||
|
||||
_nova_options = ks_loading.register_session_conf_options(
|
||||
CONF, NOVA_GROUP, deprecated_opts=deprecations)
|
||||
_ironic_options = ks_loading.register_session_conf_options(
|
||||
CONF, IRONIC_GROUP)
|
||||
|
||||
|
||||
def list_agent_opts():
|
||||
@ -141,6 +144,10 @@ def list_opts():
|
||||
itertools.chain(
|
||||
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)
|
||||
]
|
||||
|
||||
@ -318,6 +325,20 @@ def list_auth_opts():
|
||||
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():
|
||||
return [
|
||||
('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,6 +51,7 @@ pyroute2>=0.5.3;sys_platform!='win32' # Apache-2.0 (+ dual licensed GPL2)
|
||||
weakrefmethod>=1.0.2;python_version=='2.7' # PSF
|
||||
|
||||
python-novaclient>=9.1.0 # Apache-2.0
|
||||
python-ironicclient>=2.7.0 # Apache-2.0
|
||||
python-designateclient>=2.7.0 # Apache-2.0
|
||||
os-xenapi>=0.3.1 # Apache-2.0
|
||||
os-vif>=1.15.1 # Apache-2.0
|
||||
|
@ -128,6 +128,7 @@ neutron.agent.linux.pd_drivers =
|
||||
neutron.services.external_dns_drivers =
|
||||
designate = neutron.services.externaldns.drivers.designate.driver:Designate
|
||||
oslo.config.opts =
|
||||
ironic.auth = neutron.opts:list_ironic_auth_opts
|
||||
neutron = neutron.opts:list_opts
|
||||
neutron.agent = neutron.opts:list_agent_opts
|
||||
neutron.az.agent = neutron.opts:list_az_agent_opts
|
||||
|
Loading…
Reference in New Issue
Block a user