c5c18ce04f
Adds support for setting node and volume name prefixes on a per-spec basis. This allows for different node specifications to use different names, e.g. 'controller' vs 'compute'. This change also fixes an issue where node names were not globally unique, only unique on a given hypervisor. This could cause issues if used with multiple hypervisors. This has been done by rewriting the scheduling logic, replacing the 'node index' concept with a more concrete reservation of IPMI ports (which are allocated per hypervisor), and decoupling this from generation of node names (which are allocated globally). Change-Id: I929b18918c2886f42c4d05b37c81f3e63c69a92f Story: 2004894 Task: 29201 Story: 31d2681 Task: 29248
308 lines
12 KiB
Python
308 lines
12 KiB
Python
# Copyright (c) 2018 StackHPC Ltd.
|
|
#
|
|
# 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 __future__ import absolute_import
|
|
import copy
|
|
import imp
|
|
import os
|
|
|
|
from ansible.errors import AnsibleActionFail
|
|
import six
|
|
import unittest
|
|
|
|
|
|
# Python 2/3 compatibility.
|
|
try:
|
|
from unittest.mock import MagicMock
|
|
except ImportError:
|
|
from mock import MagicMock # noqa
|
|
|
|
|
|
# Import method lifted from kolla_ansible's test_merge_config.py
|
|
PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))
|
|
PLUGIN_FILE = os.path.join(PROJECT_DIR,
|
|
'ansible/action_plugins/tenks_update_state.py')
|
|
|
|
tus = imp.load_source('tenks_update_state', PLUGIN_FILE)
|
|
|
|
|
|
class TestTenksUpdateState(unittest.TestCase):
|
|
def setUp(self):
|
|
# Pass dummy arguments to allow instantiation of action plugin.
|
|
self.mod = tus.ActionModule(None, None, None, None, None, None)
|
|
self.mod.localhost_vars = {
|
|
'cmd': 'deploy',
|
|
'default_ironic_driver': 'def_ir_driv',
|
|
}
|
|
|
|
# Minimal inputs required.
|
|
self.node_types = {
|
|
'type0': {
|
|
'memory_mb': 1024,
|
|
'vcpus': 2,
|
|
'volumes': [
|
|
{
|
|
'capacity': '10GB',
|
|
},
|
|
{
|
|
'capacity': '20GB',
|
|
},
|
|
],
|
|
'physical_networks': [
|
|
'physnet0',
|
|
],
|
|
},
|
|
}
|
|
self.specs = [
|
|
{
|
|
'type': 'type0',
|
|
'count': 2,
|
|
'ironic_config': {
|
|
'resource_class': 'testrc',
|
|
},
|
|
},
|
|
]
|
|
self.hypervisor_vars = {
|
|
'foo': {
|
|
'physnet_mappings': {
|
|
'physnet0': 'dev0',
|
|
},
|
|
'ipmi_port_range_start': 100,
|
|
'ipmi_port_range_end': 102,
|
|
},
|
|
}
|
|
self.mod.args = {
|
|
'hypervisor_vars': self.hypervisor_vars,
|
|
'node_types': self.node_types,
|
|
'node_name_prefix': 'test_node_pfx',
|
|
'specs': self.specs,
|
|
'state': {},
|
|
'vol_name_prefix': 'test_vol_pfx',
|
|
}
|
|
# Alias for brevity.
|
|
self.args = self.mod.args
|
|
|
|
def test__set_physnet_idxs_no_state(self):
|
|
self.mod._set_physnet_idxs()
|
|
expected_indices = {
|
|
'physnet0': 0,
|
|
}
|
|
self.assertEqual(self.args['state']['foo']['physnet_indices'],
|
|
expected_indices)
|
|
|
|
def test__set_physnet_idxs_no_state_two_hosts(self):
|
|
self.hypervisor_vars['bar'] = self.hypervisor_vars['foo']
|
|
self.mod._set_physnet_idxs()
|
|
expected_indices = {
|
|
'physnet0': 0,
|
|
}
|
|
for hyp in {'foo', 'bar'}:
|
|
self.assertEqual(self.args['state'][hyp]['physnet_indices'],
|
|
expected_indices)
|
|
|
|
def test_set_physnet_idxs__no_state_two_hosts_different_nets(self):
|
|
self.hypervisor_vars['bar'] = self.hypervisor_vars['foo']
|
|
self.hypervisor_vars['foo']['physnet_mappings'].update({
|
|
'physnet1': 'dev1',
|
|
'physnet2': 'dev2',
|
|
})
|
|
self.hypervisor_vars['bar']['physnet_mappings'].update({
|
|
'physnet2': 'dev2',
|
|
})
|
|
self.mod._set_physnet_idxs()
|
|
for host in {'foo', 'bar'}:
|
|
idxs = list(six.itervalues(
|
|
self.args['state'][host]['physnet_indices']))
|
|
# Check all physnets have different IDs on the same host.
|
|
six.assertCountEqual(self, idxs, set(idxs))
|
|
|
|
def test_set_physnet_idxs__idx_maintained_after_removal(self):
|
|
self.hypervisor_vars['foo']['physnet_mappings'].update({
|
|
'physnet1': 'dev1',
|
|
})
|
|
self.mod._set_physnet_idxs()
|
|
physnet1_idx = self.args['state']['foo']['physnet_indices']['physnet1']
|
|
del self.hypervisor_vars['foo']['physnet_mappings']['physnet0']
|
|
self.mod._set_physnet_idxs()
|
|
self.assertEqual(
|
|
physnet1_idx,
|
|
self.args['state']['foo']['physnet_indices']['physnet1']
|
|
)
|
|
|
|
def _test__process_specs_no_state_create_nodes(self):
|
|
self.mod._process_specs()
|
|
self.assertEqual(len(self.args['state']['foo']['nodes']), 2)
|
|
return self.args['state']['foo']['nodes']
|
|
|
|
def test__process_specs_no_state_attrs(self):
|
|
nodes = self._test__process_specs_no_state_create_nodes()
|
|
for node in nodes:
|
|
self.assertTrue(node['name'].startswith('test_node_pfx'))
|
|
self.assertEqual(node['memory_mb'], 1024)
|
|
self.assertEqual(node['vcpus'], 2)
|
|
self.assertEqual(node['physical_networks'], ['physnet0'])
|
|
|
|
def test__process_specs_no_state_ipmi_ports(self):
|
|
nodes = self._test__process_specs_no_state_create_nodes()
|
|
used_ipmi_ports = set()
|
|
for node in nodes:
|
|
self.assertGreaterEqual(
|
|
node['ipmi_port'],
|
|
self.hypervisor_vars['foo']['ipmi_port_range_start']
|
|
)
|
|
self.assertLessEqual(
|
|
node['ipmi_port'],
|
|
self.hypervisor_vars['foo']['ipmi_port_range_end']
|
|
)
|
|
self.assertNotIn(node['ipmi_port'], used_ipmi_ports)
|
|
used_ipmi_ports.add(node['ipmi_port'])
|
|
|
|
def test__process_specs_no_state_volumes(self):
|
|
nodes = self._test__process_specs_no_state_create_nodes()
|
|
for node in nodes:
|
|
self.assertEqual(len(node['volumes']), 2)
|
|
for n in {'0', '1'}:
|
|
self.assertIn(node['name'] + 'test_vol_pfx' + n,
|
|
[vol['name'] for vol in node['volumes']])
|
|
for c in {'10GB', '20GB'}:
|
|
self.assertIn(c, [vol['capacity'] for vol in node['volumes']])
|
|
|
|
def test__process_specs_apply_twice(self):
|
|
self.mod._process_specs()
|
|
created_state = copy.deepcopy(self.args['state'])
|
|
self.mod._process_specs()
|
|
self.assertEqual(created_state, self.args['state'])
|
|
|
|
def test__process_specs_multiple_hosts(self):
|
|
self.hypervisor_vars['bar'] = self.hypervisor_vars['foo']
|
|
self.mod._process_specs()
|
|
foo_nodes = self.args['state']['foo']['nodes']
|
|
bar_nodes = self.args['state']['bar']['nodes']
|
|
names = {foo_nodes[0]['name'], bar_nodes[0]['name']}
|
|
self.assertEqual(names, {'test_node_pfx0', 'test_node_pfx1'})
|
|
|
|
def test__process_specs_unnecessary_node(self):
|
|
# Create some nodes definitions.
|
|
self.mod._process_specs()
|
|
|
|
# Add another node to the state that isn't required.
|
|
self.args['state']['foo']['nodes'].append(copy.deepcopy(
|
|
self.args['state']['foo']['nodes'][0]))
|
|
self.args['state']['foo']['nodes'][-1]['vcpus'] = 42
|
|
new_node = copy.deepcopy(self.args['state']['foo']['nodes'][-1])
|
|
|
|
self.mod._process_specs()
|
|
# Check that node has been marked for deletion.
|
|
self.assertNotIn(new_node, self.args['state']['foo']['nodes'])
|
|
new_node['state'] = 'absent'
|
|
self.assertIn(new_node, self.args['state']['foo']['nodes'])
|
|
|
|
def test__process_specs_teardown(self):
|
|
# Create some node definitions.
|
|
self.mod._process_specs()
|
|
|
|
# After teardown, we expected all created definitions to now have an
|
|
# 'absent' state.
|
|
expected_state = copy.deepcopy(self.args['state'])
|
|
for node in expected_state['foo']['nodes']:
|
|
node['state'] = 'absent'
|
|
self.mod.localhost_vars['cmd'] = 'teardown'
|
|
|
|
# After one or more runs, the 'absent' state nodes should still exist,
|
|
# since they're only removed after completion of deployment in a
|
|
# playbook.
|
|
for _ in six.moves.range(3):
|
|
self.mod._process_specs()
|
|
self.assertEqual(expected_state, self.args['state'])
|
|
|
|
def test__process_specs_no_hypervisors(self):
|
|
self.args['hypervisor_vars'] = {}
|
|
self.assertRaises(AnsibleActionFail, self.mod._process_specs)
|
|
|
|
def test__process_specs_no_hypervisors_on_physnet(self):
|
|
self.node_types['type0']['physical_networks'].append('another_pn')
|
|
self.assertRaises(AnsibleActionFail, self.mod._process_specs)
|
|
|
|
def test__process_specs_one_hypervisor_on_physnet(self):
|
|
self.node_types['type0']['physical_networks'].append('another_pn')
|
|
self.hypervisor_vars['bar'] = copy.deepcopy(
|
|
self.hypervisor_vars['foo'])
|
|
self.hypervisor_vars['bar']['physnet_mappings']['another_pn'] = 'dev1'
|
|
self.mod._process_specs()
|
|
|
|
# Check all nodes were scheduled to the hypervisor connected to the
|
|
# new physnet.
|
|
self.assertEqual(len(self.args['state']['foo']['nodes']), 0)
|
|
self.assertEqual(len(self.args['state']['bar']['nodes']), 2)
|
|
|
|
def test__process_specs_not_enough_ports(self):
|
|
# Give 'foo' only a single IPMI port to allocate.
|
|
self.hypervisor_vars['foo']['ipmi_port_range_start'] = 123
|
|
self.hypervisor_vars['foo']['ipmi_port_range_end'] = 123
|
|
self.assertRaises(AnsibleActionFail, self.mod._process_specs)
|
|
|
|
def test__process_specs_node_name_prefix(self):
|
|
self.specs[0]['node_name_prefix'] = 'foo-prefix'
|
|
self.mod._process_specs()
|
|
foo_nodes = self.args['state']['foo']['nodes']
|
|
self.assertEqual(foo_nodes[0]['name'], 'foo-prefix0')
|
|
self.assertEqual(foo_nodes[1]['name'], 'foo-prefix1')
|
|
|
|
def test__process_specs_node_name_prefix_multiple_specs(self):
|
|
self.specs[0]['node_name_prefix'] = 'foo-prefix'
|
|
self.specs.append({
|
|
'type': 'type0',
|
|
'count': 1,
|
|
'ironic_config': {
|
|
'resource_class': 'testrc',
|
|
},
|
|
})
|
|
self.mod._process_specs()
|
|
foo_nodes = self.args['state']['foo']['nodes']
|
|
self.assertEqual(foo_nodes[0]['name'], 'foo-prefix0')
|
|
self.assertEqual(foo_nodes[1]['name'], 'foo-prefix1')
|
|
self.assertEqual(foo_nodes[2]['name'], 'test_node_pfx0')
|
|
|
|
def test__process_specs_node_name_prefix_multiple_hosts(self):
|
|
self.specs[0]['node_name_prefix'] = 'foo-prefix'
|
|
self.hypervisor_vars['bar'] = self.hypervisor_vars['foo']
|
|
self.mod._process_specs()
|
|
foo_nodes = self.args['state']['foo']['nodes']
|
|
bar_nodes = self.args['state']['bar']['nodes']
|
|
names = {foo_nodes[0]['name'], bar_nodes[0]['name']}
|
|
self.assertEqual(names, {'foo-prefix0', 'foo-prefix1'})
|
|
|
|
def test__process_specs_vol_name_prefix(self):
|
|
self.specs[0]['vol_name_prefix'] = 'foo-prefix'
|
|
self.mod._process_specs()
|
|
foo_nodes = self.args['state']['foo']['nodes']
|
|
self.assertEqual(foo_nodes[0]['volumes'][0]['name'],
|
|
'test_node_pfx0foo-prefix0')
|
|
self.assertEqual(foo_nodes[0]['volumes'][1]['name'],
|
|
'test_node_pfx0foo-prefix1')
|
|
self.assertEqual(foo_nodes[1]['volumes'][0]['name'],
|
|
'test_node_pfx1foo-prefix0')
|
|
self.assertEqual(foo_nodes[1]['volumes'][1]['name'],
|
|
'test_node_pfx1foo-prefix1')
|
|
|
|
def test__prune_absent_nodes(self):
|
|
# Create some node definitions.
|
|
self.mod._process_specs()
|
|
# Set them to be 'absent'.
|
|
for node in self.args['state']['foo']['nodes']:
|
|
node['state'] = 'absent'
|
|
self.mod._prune_absent_nodes()
|
|
# Ensure they were removed.
|
|
self.assertEqual(self.args['state']['foo']['nodes'], [])
|