3 Commits

Author SHA1 Message Date
Hollie Hutchinson
ee88cc15b8 Remove MAC address requirement for node registration
Allow the creation of baremetal nodes without specifying any NICs.
In this case switch the target provision state from available to
enroll.

We're proposing this patch to support enrolling nodes as a first
step before running an out-of-band Redfish inspection to get the
NICs.

No change is made to existing workflows.

Change-Id: I9b1636d778e7477191bfbe6094bd6bc70a4d384e
Signed-off-by: Hollie Hutchinson <hollie@stackhpc.com>
Signed-off-by: Doug Szumski <doug@stackhpc.com>
2026-07-23 16:36:27 +01:00
Zuul
209cf2d605 Merge "Revive collection unit tests" 2026-06-15 16:05:50 +00:00
Dmitriy Rabotyagov
dc2bb8232e Revive collection unit tests
Seemingly, ansible-test unit tests were not run in CI for a while and
are broken in multiple places.

This patch aims to re-incarnate unit testing of modules
and execute them as part of the sanity tests.
We use sanity right now, as they perform testing against different
ansible versions and unit tests take multiple seconds to complete.
So spawning a separate nodes would be an overkill.

Unit tests have also detected a regression in server module, as existing
nets generation can create a generator, where `isinstance(net, dict)`
will return negative result, as it is gonna
be a generator, not a dict.
So this part has been refactored to avoid poitential regressions.

Change-Id: Ibefc087239b5c8d5e843c977c2c5a2250ae0bfbd
Signed-off-by: Dmitriy Rabotyagov <dmitriy.rabotyagov@cleura.com>
2026-06-10 17:09:00 +00:00
5 changed files with 159 additions and 27 deletions

View File

@@ -75,7 +75,10 @@ options:
description: description:
- 'A list of network interface cards, eg, C( - mac: aa:bb:cc:aa:bb:cc)' - 'A list of network interface cards, eg, C( - mac: aa:bb:cc:aa:bb:cc)'
- This node attribute cannot be updated. - This node attribute cannot be updated.
required: true - If this option is not set, the target provision state of the node
changes from available to enroll.
required: false
default: []
type: list type: list
elements: dict elements: dict
suboptions: suboptions:
@@ -508,7 +511,7 @@ class BaremetalNodeModule(OpenStackModule):
management_interface=dict(), management_interface=dict(),
name=dict(), name=dict(),
network_interface=dict(), network_interface=dict(),
nics=dict(type='list', required=True, elements='dict'), nics=dict(type='list', default=[], elements='dict'),
power_interface=dict(), power_interface=dict(),
properties=dict( properties=dict(
type='dict', type='dict',
@@ -655,6 +658,8 @@ class BaremetalNodeModule(OpenStackModule):
nics=self.params['nics'], nics=self.params['nics'],
wait=self.params['wait'], wait=self.params['wait'],
timeout=self.params['timeout'], timeout=self.params['timeout'],
provision_state=(
'available' if len(self.params['nics']) > 0 else 'enroll'),
**kwargs) **kwargs)
self.exit_json(changed=True, node=node.to_dict(computed=False)) self.exit_json(changed=True, node=node.to_dict(computed=False))

View File

@@ -1228,10 +1228,13 @@ class ServerModule(OpenStackModule):
if not isinstance(stringified_nets, list): if not isinstance(stringified_nets, list):
self.fail_json(msg="The 'nics' parameter must be a list.") self.fail_json(msg="The 'nics' parameter must be a list.")
nets = [(dict((nested_net.split('='),)) nets = []
for nested_net in net.split(',')) for net in stringified_nets:
if isinstance(net, str) else net if isinstance(net, str):
for net in stringified_nets] for nested_net in net.split(','):
nets.append(dict((nested_net.split('='),)))
else:
nets.append(net)
for net in nets: for net in nets:
if not isinstance(net, dict): if not isinstance(net, dict):

View File

@@ -16,13 +16,48 @@ class AnsibleExit(Exception):
pass pass
class FakeSDK(object):
class exceptions(object):
class OpenStackCloudException(Exception):
pass
class ResourceNotFound(OpenStackCloudException):
pass
class HelperServerModule(os_server.ServerModule):
def __init__(self, params, conn, sdk):
self.params = {}
for k, v in self.argument_spec.items():
if 'default' in v:
self.params[k] = v['default']
else:
self.params[k] = None
self.params.update(params)
if ('floating_ips' in params or 'floating_ip_pools' in params) and 'auto_ip' not in params:
self.params['auto_ip'] = False
self.params = collections.defaultdict(str, self.params)
self.conn = conn
self.sdk = sdk
self.module_name = 'server'
self.check_mode = False
self.results = {'changed': False}
self.ansible = mock.MagicMock()
self.ansible.check_mode = False
self.ansible.exit_json.side_effect = AnsibleExit
self.ansible.fail_json.side_effect = AnsibleFail
self.exit_json = self.ansible.exit_json
self.fail_json = self.ansible.fail_json
def params_from_doc(func): def params_from_doc(func):
'''This function extracts the docstring from the specified function, '''This function extracts the docstring from the specified function,
parses it as a YAML document, and returns parameters for the openstack.cloud.server parses it as a YAML document, and returns parameters for the openstack.cloud.server
module.''' module.'''
doc = inspect.getdoc(func) doc = inspect.getdoc(func)
cfg = yaml.load(doc) cfg = yaml.safe_load(doc)
for task in cfg: for task in cfg:
for module, params in task.items(): for module, params in task.items():
@@ -95,15 +130,32 @@ class TestNetworkArgs(object):
def setup_method(self, method): def setup_method(self, method):
self.cloud = FakeCloud() self.cloud = FakeCloud()
self.module = mock.MagicMock() self.cloud.network = mock.MagicMock()
self.module.params = params_from_doc(method)
def find_network(name, ignore_missing=False):
net = self.cloud.get_network(name)
if net:
return mock.MagicMock(id=net['id'])
raise Exception("Network not found")
def find_port(name, ignore_missing=False):
port = self.cloud.get_port(name)
if port:
return mock.MagicMock(id=port['id'])
raise Exception("Port not found")
self.cloud.network.find_network.side_effect = find_network
self.cloud.network.find_port.side_effect = find_port
self.params = params_from_doc(method)
self.module = HelperServerModule(self.params, self.cloud, FakeSDK())
def test_nics_string_net_id(self): def test_nics_string_net_id(self):
''' '''
- openstack.cloud.server: - openstack.cloud.server:
nics: net-id=1234 nics: net-id=1234
''' '''
args = os_server._network_args(self.module, self.cloud) args = self.module._parse_nics()
assert args[0]['net-id'] == '1234' assert args[0]['net-id'] == '1234'
def test_nics_string_net_id_list(self): def test_nics_string_net_id_list(self):
@@ -111,7 +163,7 @@ class TestNetworkArgs(object):
- openstack.cloud.server: - openstack.cloud.server:
nics: net-id=1234,net-id=4321 nics: net-id=1234,net-id=4321
''' '''
args = os_server._network_args(self.module, self.cloud) args = self.module._parse_nics()
assert args[0]['net-id'] == '1234' assert args[0]['net-id'] == '1234'
assert args[1]['net-id'] == '4321' assert args[1]['net-id'] == '4321'
@@ -120,7 +172,7 @@ class TestNetworkArgs(object):
- openstack.cloud.server: - openstack.cloud.server:
nics: port-id=1234 nics: port-id=1234
''' '''
args = os_server._network_args(self.module, self.cloud) args = self.module._parse_nics()
assert args[0]['port-id'] == '1234' assert args[0]['port-id'] == '1234'
def test_nics_string_net_name(self): def test_nics_string_net_name(self):
@@ -128,7 +180,7 @@ class TestNetworkArgs(object):
- openstack.cloud.server: - openstack.cloud.server:
nics: net-name=network1 nics: net-name=network1
''' '''
args = os_server._network_args(self.module, self.cloud) args = self.module._parse_nics()
assert args[0]['net-id'] == '5678' assert args[0]['net-id'] == '5678'
def test_nics_string_port_name(self): def test_nics_string_port_name(self):
@@ -136,7 +188,7 @@ class TestNetworkArgs(object):
- openstack.cloud.server: - openstack.cloud.server:
nics: port-name=port1 nics: port-name=port1
''' '''
args = os_server._network_args(self.module, self.cloud) args = self.module._parse_nics()
assert args[0]['port-id'] == '1234' assert args[0]['port-id'] == '1234'
def test_nics_structured_net_id(self): def test_nics_structured_net_id(self):
@@ -145,7 +197,7 @@ class TestNetworkArgs(object):
nics: nics:
- net-id: '1234' - net-id: '1234'
''' '''
args = os_server._network_args(self.module, self.cloud) args = self.module._parse_nics()
assert args[0]['net-id'] == '1234' assert args[0]['net-id'] == '1234'
def test_nics_structured_mixed(self): def test_nics_structured_mixed(self):
@@ -156,7 +208,7 @@ class TestNetworkArgs(object):
- port-name: port1 - port-name: port1
- 'net-name=network1,port-id=4321' - 'net-name=network1,port-id=4321'
''' '''
args = os_server._network_args(self.module, self.cloud) args = self.module._parse_nics()
assert args[0]['net-id'] == '1234' assert args[0]['net-id'] == '1234'
assert args[1]['port-id'] == '1234' assert args[1]['port-id'] == '1234'
assert args[2]['net-id'] == '5678' assert args[2]['net-id'] == '5678'
@@ -166,10 +218,31 @@ class TestNetworkArgs(object):
class TestCreateServer(object): class TestCreateServer(object):
def setup_method(self, method): def setup_method(self, method):
self.cloud = FakeCloud() self.cloud = FakeCloud()
self.module = mock.MagicMock() self.cloud.compute = mock.MagicMock()
self.module.params = params_from_doc(method) self.cloud.compute.find_server.return_value = None
self.module.fail_json.side_effect = AnsibleFail() self.cloud.compute.get_server.return_value = mock.MagicMock()
self.module.exit_json.side_effect = AnsibleExit() self.cloud.compute.get_server.return_value.to_dict.return_value = {'id': '1234'}
def find_flavor(name_or_id, ignore_missing=True):
flavor = self.cloud.get_flavor(name_or_id)
if flavor:
return mock.MagicMock(id=flavor['id'])
if not ignore_missing:
raise FakeSDK.exceptions.ResourceNotFound("Could not find flavor {0}".format(name_or_id))
return None
self.cloud.compute.find_flavor.side_effect = find_flavor
self.cloud.network = mock.MagicMock()
def find_network(name, ignore_missing=False):
net = self.cloud.get_network(name)
if net:
return mock.MagicMock(id=net['id'])
raise Exception("Network not found")
self.cloud.network.find_network.side_effect = find_network
self.params = params_from_doc(method)
self.module = HelperServerModule(self.params, self.cloud, FakeSDK())
self.meta = mock.MagicMock() self.meta = mock.MagicMock()
self.meta.gett_hostvars_from_server.return_value = { self.meta.gett_hostvars_from_server.return_value = {
@@ -188,7 +261,7 @@ class TestCreateServer(object):
- key: value - key: value
''' '''
with pytest.raises(AnsibleExit): with pytest.raises(AnsibleExit):
os_server._create_server(self.module, self.cloud) self.module()
assert self.cloud.create_server.call_count == 1 assert self.cloud.create_server.call_count == 1
assert self.cloud.create_server.call_args[1]['image'] == self.cloud.get_image_id('cirros') assert self.cloud.create_server.call_args[1]['image'] == self.cloud.get_image_id('cirros')
@@ -204,7 +277,7 @@ class TestCreateServer(object):
- net-name: network1 - net-name: network1
''' '''
with pytest.raises(AnsibleFail): with pytest.raises(AnsibleFail):
os_server._create_server(self.module, self.cloud) self.module()
assert 'missing_flavor' in self.module.fail_json.call_args[1]['msg'] assert 'missing_flavor' in self.module.fail_json.call_args[1]['msg']
@@ -216,8 +289,12 @@ class TestCreateServer(object):
nics: nics:
- net-name: missing_network - net-name: missing_network
''' '''
def find_network_fail(name, ignore_missing=False):
raise FakeSDK.exceptions.ResourceNotFound("missing_network")
self.cloud.network.find_network.side_effect = find_network_fail
with pytest.raises(AnsibleFail): with pytest.raises(AnsibleFail):
os_server._create_server(self.module, self.cloud) self.module()
assert 'missing_network' in self.module.fail_json.call_args[1]['msg'] assert 'missing_network' in self.module.fail_json.call_args[1]['msg']
@@ -231,7 +308,7 @@ class TestCreateServer(object):
- net-name: network1 - net-name: network1
''' '''
with pytest.raises(AnsibleFail): with pytest.raises(AnsibleFail):
os_server._create_server(self.module, self.cloud) self.module()
assert 'auto_ip' in self.module.fail_json.call_args[1]['msg'] assert 'auto_ip' in self.module.fail_json.call_args[1]['msg']
@@ -245,7 +322,7 @@ class TestCreateServer(object):
- net-name: network1 - net-name: network1
''' '''
with pytest.raises(AnsibleFail): with pytest.raises(AnsibleFail):
os_server._create_server(self.module, self.cloud) self.module()
assert 'floating_ips' in self.module.fail_json.call_args[1]['msg'] assert 'floating_ips' in self.module.fail_json.call_args[1]['msg']
@@ -259,6 +336,6 @@ class TestCreateServer(object):
- net-name: network1 - net-name: network1
''' '''
with pytest.raises(AnsibleFail): with pytest.raises(AnsibleFail):
os_server._create_server(self.module, self.cloud) self.module()
assert 'floating_ip_pools' in self.module.fail_json.call_args[1]['msg'] assert 'floating_ip_pools' in self.module.fail_json.call_args[1]['msg']

View File

@@ -53,16 +53,59 @@ hostvars = {
} }
try:
from ansible._internal._templating._engine import TrustedAsTemplate
has_trusted = True
except ImportError:
has_trusted = False
def tag_trusted(val):
if not has_trusted:
return val
if isinstance(val, dict):
return {k: tag_trusted(v) for k, v in val.items()}
elif isinstance(val, list):
return [tag_trusted(v) for v in val]
elif isinstance(val, str):
return TrustedAsTemplate().tag(val)
return val
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def inventory(): def inventory():
inventory = InventoryModule() inventory = InventoryModule()
inventory._config_data = config_data inventory._config_data = tag_trusted(config_data)
inventory.inventory = InventoryData() inventory.inventory = InventoryData()
inventory.templar = Templar(loader=None) inventory.templar = Templar(loader=None)
for host in hostvars: for host in hostvars:
inventory.inventory.add_host(host) inventory.inventory.add_host(host)
def _set_variables(hostvars, groups):
for host in hostvars:
try:
inventory._set_composite_vars(
inventory._config_data.get('compose'), hostvars[host], host, strict=True)
except Exception as e:
print("set_composite_vars error: %s" % e)
raise
for key in hostvars[host]:
inventory.inventory.set_variable(host, key, hostvars[host][key])
try:
inventory._add_host_to_composed_groups(
inventory._config_data.get('groups'), hostvars[host], host, strict=True)
except Exception as e:
print("add_host_to_composed_groups error: %s" % e)
raise
try:
inventory._add_host_to_keyed_groups(
inventory._config_data.get('keyed_groups'), hostvars[host], host, strict=True)
except Exception as e:
print("add_host_to_keyed_groups error: %s" % e)
raise
inventory._set_variables = _set_variables
return inventory return inventory

View File

@@ -44,3 +44,7 @@ ansible-test sanity -v \
--python ${PY_VER} \ --python ${PY_VER} \
$SKIP_TESTS \ $SKIP_TESTS \
plugins/ docs/ meta/ plugins/ docs/ meta/
ansible-test units -v \
--venv \
--python ${PY_VER}