Browse Source
Add new node_not_found_hook - enroll_node_not_found hook, which allows to enroll unknown nodes to Ironic automatically. Change-Id: If1528688504e4be4b2369b985bc576544d96868d Related-Bug: #1524753changes/12/281312/9
13 changed files with 389 additions and 7 deletions
@ -0,0 +1,101 @@
|
||||
# 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. |
||||
|
||||
"""Enroll node not found hook hook.""" |
||||
|
||||
from oslo_config import cfg |
||||
|
||||
from ironic_inspector.common.i18n import _, _LW |
||||
from ironic_inspector import node_cache |
||||
from ironic_inspector import utils |
||||
|
||||
|
||||
DISCOVERY_OPTS = [ |
||||
cfg.StrOpt('enroll_node_driver', |
||||
default='fake', |
||||
help='The name of the Ironic driver used by the enroll ' |
||||
'hook when creating a new node in Ironic.'), |
||||
] |
||||
|
||||
|
||||
def list_opts(): |
||||
return [ |
||||
('discovery', DISCOVERY_OPTS) |
||||
] |
||||
|
||||
CONF = cfg.CONF |
||||
CONF.register_opts(DISCOVERY_OPTS, group='discovery') |
||||
|
||||
LOG = utils.getProcessingLogger(__name__) |
||||
|
||||
|
||||
def _extract_node_driver_info(introspection_data): |
||||
node_driver_info = {} |
||||
ipmi_address = utils.get_ipmi_address_from_data(introspection_data) |
||||
if ipmi_address: |
||||
node_driver_info['ipmi_address'] = ipmi_address |
||||
else: |
||||
LOG.warning(_LW('No BMC address provided, discovered node will be ' |
||||
'created without ipmi address')) |
||||
return node_driver_info |
||||
|
||||
|
||||
def _check_existing_nodes(introspection_data, node_driver_info, ironic): |
||||
macs = introspection_data.get('macs') |
||||
if macs: |
||||
# verify existing ports |
||||
for mac in macs: |
||||
ports = ironic.port.list(address=mac) |
||||
if not ports: |
||||
continue |
||||
raise utils.Error( |
||||
_('Port %(mac)s already exists, uuid: %(uuid)s') % |
||||
{'mac': mac, 'uuid': ports.uuid}, data=introspection_data) |
||||
else: |
||||
LOG.warning(_LW('No suitable interfaces found for discovered node. ' |
||||
'Check that validate_interfaces hook is listed in ' |
||||
'[processing]default_processing_hooks config option')) |
||||
|
||||
# verify existing node with discovered ipmi address |
||||
ipmi_address = node_driver_info.get('ipmi_address') |
||||
if ipmi_address: |
||||
# FIXME(aarefiev): it's not effective to fetch all nodes, and may |
||||
# impact on performance on big clusters |
||||
nodes = ironic.node.list(fields=('uuid', 'driver_info'), limit=0) |
||||
for node in nodes: |
||||
if ipmi_address == utils.get_ipmi_address(node): |
||||
raise utils.Error( |
||||
_('Node %(uuid)s already has BMC address ' |
||||
'%(ipmi_address)s, not enrolling') % |
||||
{'ipmi_address': ipmi_address, 'uuid': node.uuid}, |
||||
data=introspection_data) |
||||
|
||||
|
||||
def enroll_node_not_found_hook(introspection_data, **kwargs): |
||||
node_attr = {} |
||||
ironic = utils.get_client() |
||||
|
||||
node_driver_info = _extract_node_driver_info(introspection_data) |
||||
node_attr['driver_info'] = node_driver_info |
||||
|
||||
node_driver = CONF.discovery.enroll_node_driver |
||||
|
||||
_check_existing_nodes(introspection_data, node_driver_info, ironic) |
||||
LOG.debug('Creating discovered node with driver %(driver)s and ' |
||||
'attributes: %(attr)s', |
||||
{'driver': node_driver, 'attr': node_attr}, |
||||
data=introspection_data) |
||||
# NOTE(aarefiev): This flag allows to distinguish enrolled manually |
||||
# and auto-discovered nodes in the introspection rules. |
||||
introspection_data['auto_discovered'] = True |
||||
return node_cache.create_node(node_driver, ironic=ironic, **node_attr) |
@ -0,0 +1,127 @@
|
||||
# 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 copy |
||||
import mock |
||||
|
||||
from ironic_inspector import node_cache |
||||
from ironic_inspector.plugins import discovery |
||||
from ironic_inspector.test import base as test_base |
||||
from ironic_inspector import utils |
||||
|
||||
|
||||
def copy_call_args(mock_arg): |
||||
new_mock = mock.Mock() |
||||
|
||||
def side_effect(*args, **kwargs): |
||||
args = copy.deepcopy(args) |
||||
kwargs = copy.deepcopy(kwargs) |
||||
new_mock(*args, **kwargs) |
||||
return mock.DEFAULT |
||||
mock_arg.side_effect = side_effect |
||||
return new_mock |
||||
|
||||
|
||||
class TestEnrollNodeNotFoundHook(test_base.NodeTest): |
||||
def setUp(self): |
||||
super(TestEnrollNodeNotFoundHook, self).setUp() |
||||
self.ironic = mock.MagicMock() |
||||
|
||||
@mock.patch.object(node_cache, 'create_node', autospec=True) |
||||
@mock.patch.object(utils, 'get_client', autospec=True) |
||||
@mock.patch.object(discovery, '_check_existing_nodes', autospec=True) |
||||
def test_enroll_default(self, mock_check_existing, mock_client, |
||||
mock_create_node): |
||||
mock_client.return_value = self.ironic |
||||
introspection_data = {'test': 'test'} |
||||
|
||||
discovery.enroll_node_not_found_hook(introspection_data) |
||||
|
||||
mock_create_node.assert_called_once_with('fake', ironic=self.ironic, |
||||
driver_info={}) |
||||
mock_check_existing.assert_called_once_with( |
||||
introspection_data, {}, self.ironic) |
||||
|
||||
@mock.patch.object(node_cache, 'create_node', autospec=True) |
||||
@mock.patch.object(utils, 'get_client', autospec=True) |
||||
@mock.patch.object(discovery, '_check_existing_nodes', autospec=True) |
||||
def test_enroll_with_ipmi_address(self, mock_check_existing, mock_client, |
||||
mock_create_node): |
||||
mock_client.return_value = self.ironic |
||||
introspection_data = {'ipmi_address': '1.2.3.4'} |
||||
expected_data = introspection_data.copy() |
||||
mock_check_existing = copy_call_args(mock_check_existing) |
||||
|
||||
discovery.enroll_node_not_found_hook(introspection_data) |
||||
|
||||
mock_create_node.assert_called_once_with( |
||||
'fake', ironic=self.ironic, |
||||
driver_info={'ipmi_address': '1.2.3.4'}) |
||||
mock_check_existing.assert_called_once_with( |
||||
expected_data, {'ipmi_address': '1.2.3.4'}, self.ironic) |
||||
self.assertEqual({'ipmi_address': '1.2.3.4', 'auto_discovered': True}, |
||||
introspection_data) |
||||
|
||||
@mock.patch.object(node_cache, 'create_node', autospec=True) |
||||
@mock.patch.object(utils, 'get_client', autospec=True) |
||||
@mock.patch.object(discovery, '_check_existing_nodes', autospec=True) |
||||
def test_enroll_with_non_default_driver(self, mock_check_existing, |
||||
mock_client, mock_create_node): |
||||
mock_client.return_value = self.ironic |
||||
discovery.CONF.set_override('enroll_node_driver', 'fake2', |
||||
'discovery') |
||||
mock_check_existing = copy_call_args(mock_check_existing) |
||||
introspection_data = {} |
||||
|
||||
discovery.enroll_node_not_found_hook(introspection_data) |
||||
|
||||
mock_create_node.assert_called_once_with('fake2', ironic=self.ironic, |
||||
driver_info={}) |
||||
mock_check_existing.assert_called_once_with( |
||||
{}, {}, self.ironic) |
||||
self.assertEqual({'auto_discovered': True}, introspection_data) |
||||
|
||||
def test__check_existing_nodes_new_mac(self): |
||||
self.ironic.port.list.return_value = [] |
||||
introspection_data = {'macs': self.macs} |
||||
node_driver_info = {} |
||||
|
||||
discovery._check_existing_nodes( |
||||
introspection_data, node_driver_info, self.ironic) |
||||
|
||||
def test__check_existing_nodes_existing_mac(self): |
||||
self.ironic.port.list.return_value = mock.MagicMock( |
||||
address=self.macs[0], uuid='fake_port') |
||||
introspection_data = {'macs': self.macs} |
||||
node_driver_info = {} |
||||
|
||||
self.assertRaises(utils.Error, |
||||
discovery._check_existing_nodes, |
||||
introspection_data, node_driver_info, self.ironic) |
||||
|
||||
def test__check_existing_nodes_new_node(self): |
||||
self.ironic.node.list.return_value = [mock.MagicMock( |
||||
driver_info={'ipmi_address': '1.2.4.3'}, uuid='fake_node')] |
||||
introspection_data = {} |
||||
node_driver_info = {'ipmi_address': self.bmc_address} |
||||
|
||||
discovery._check_existing_nodes(introspection_data, node_driver_info, |
||||
self.ironic) |
||||
|
||||
def test__check_existing_nodes_existing_node(self): |
||||
self.ironic.node.list.return_value = [mock.MagicMock( |
||||
driver_info={'ipmi_address': self.bmc_address}, uuid='fake_node')] |
||||
introspection_data = {} |
||||
node_driver_info = {'ipmi_address': self.bmc_address} |
||||
|
||||
self.assertRaises(utils.Error, discovery._check_existing_nodes, |
||||
introspection_data, node_driver_info, self.ironic) |
@ -0,0 +1,6 @@
|
||||
--- |
||||
upgrade: |
||||
- Switch required Ironic API version to '1.11', which supports 'enroll' state. |
||||
features: |
||||
- Add a new node_not_found hook - enroll, which allows automatically discover |
||||
Ironic's node. |
Loading…
Reference in new issue