Implement verify profiles as a custom action
Adds a custom action to verify the number of nodes with proper profiles exist in the baremetal inventory. Change-Id: If6460877188af4d006df5f75ab5a921b411088e2 Partial-Bug: #1638697
This commit is contained in:
4
releasenotes/notes/verify-profiles-a9d075f565bc3df6.yaml
Normal file
4
releasenotes/notes/verify-profiles-a9d075f565bc3df6.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Adds an action and workflow used to verify the profiles
|
||||||
|
assigned to nodes and their count.
|
@@ -103,5 +103,6 @@ mistral.actions =
|
|||||||
tripleo.validations.list_groups = tripleo_common.actions.validations:ListGroupsAction
|
tripleo.validations.list_groups = tripleo_common.actions.validations:ListGroupsAction
|
||||||
tripleo.validations.list_validations = tripleo_common.actions.validations:ListValidationsAction
|
tripleo.validations.list_validations = tripleo_common.actions.validations:ListValidationsAction
|
||||||
tripleo.validations.run_validation = tripleo_common.actions.validations:RunValidationAction
|
tripleo.validations.run_validation = tripleo_common.actions.validations:RunValidationAction
|
||||||
|
tripleo.validations.verify_profiles = tripleo_common.actions.validations:VerifyProfilesAction
|
||||||
# deprecated for pike release, will be removed in queens
|
# deprecated for pike release, will be removed in queens
|
||||||
tripleo.templates.upload_default = tripleo_common.actions.templates:UploadTemplatesAction
|
tripleo.templates.upload_default = tripleo_common.actions.templates:UploadTemplatesAction
|
||||||
|
@@ -295,3 +295,92 @@ class CheckNodeBootConfigurationAction(base.TripleOAction):
|
|||||||
mistral_result = {'data': return_value}
|
mistral_result = {'data': return_value}
|
||||||
|
|
||||||
return mistral_workflow_utils.Result(**mistral_result)
|
return mistral_workflow_utils.Result(**mistral_result)
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyProfilesAction(base.TripleOAction):
|
||||||
|
"""Verify that the profiles have enough nodes"""
|
||||||
|
|
||||||
|
# TODO(bcrochet): The validation actions are temporary. This logic should
|
||||||
|
# move to the tripleo-validations project eventually.
|
||||||
|
def __init__(self, nodes, flavors):
|
||||||
|
super(VerifyProfilesAction, self).__init__()
|
||||||
|
|
||||||
|
self.nodes = nodes
|
||||||
|
self.flavors = flavors
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
errors = []
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
bm_nodes = {node['uuid']: node for node in self.nodes
|
||||||
|
if node['provision_state'] in ('available', 'active')}
|
||||||
|
free_node_caps = {uu: self._node_get_capabilities(node)
|
||||||
|
for uu, node in bm_nodes.items()}
|
||||||
|
|
||||||
|
profile_flavor_used = False
|
||||||
|
for flavor_name, (flavor, scale) in self.flavors.items():
|
||||||
|
if not scale:
|
||||||
|
continue
|
||||||
|
|
||||||
|
profile = flavor.get('capabilities:profile')
|
||||||
|
|
||||||
|
if not profile and len(self.flavors) > 1:
|
||||||
|
message = ('Error: The {flavor} flavor has no profile '
|
||||||
|
'associated.\n'
|
||||||
|
'Recommendation: assign a profile with openstack '
|
||||||
|
'flavor set --property '
|
||||||
|
'"capabilities:profile"="PROFILE_NAME" {flavor}')
|
||||||
|
|
||||||
|
errors.append(message.format(flavor=flavor_name))
|
||||||
|
continue
|
||||||
|
|
||||||
|
profile_flavor_used = True
|
||||||
|
|
||||||
|
assigned_nodes = [uu for uu, caps in free_node_caps.items()
|
||||||
|
if caps.get('profile') == profile]
|
||||||
|
required_count = scale - len(assigned_nodes)
|
||||||
|
|
||||||
|
if required_count < 0:
|
||||||
|
warnings.append('%d nodes with profile %s won\'t be used '
|
||||||
|
'for deployment now' % (-required_count,
|
||||||
|
profile))
|
||||||
|
required_count = 0
|
||||||
|
|
||||||
|
for uu in assigned_nodes:
|
||||||
|
free_node_caps.pop(uu)
|
||||||
|
|
||||||
|
if required_count > 0:
|
||||||
|
message = ('Error: only {total} of {scale} requested ironic '
|
||||||
|
'nodes are tagged to profile {profile} (for flavor '
|
||||||
|
'{flavor}).\n'
|
||||||
|
'Recommendation: tag more nodes using openstack '
|
||||||
|
'baremetal node set --property "capabilities='
|
||||||
|
'profile:{profile},boot_option:local" <NODE ID>')
|
||||||
|
errors.append(message.format(total=scale - required_count,
|
||||||
|
scale=scale,
|
||||||
|
profile=profile,
|
||||||
|
flavor=flavor_name))
|
||||||
|
|
||||||
|
nodes_without_profile = [uu for uu, caps in free_node_caps.items()
|
||||||
|
if not caps.get('profile')]
|
||||||
|
if nodes_without_profile and profile_flavor_used:
|
||||||
|
warnings.append("There are %d ironic nodes with no profile that "
|
||||||
|
"will not be used: %s" % (
|
||||||
|
len(nodes_without_profile),
|
||||||
|
', '.join(nodes_without_profile)))
|
||||||
|
|
||||||
|
return_value = {
|
||||||
|
'errors': errors,
|
||||||
|
'warnings': warnings,
|
||||||
|
}
|
||||||
|
if errors:
|
||||||
|
mistral_result = {'error': return_value}
|
||||||
|
else:
|
||||||
|
mistral_result = {'data': return_value}
|
||||||
|
|
||||||
|
return mistral_workflow_utils.Result(**mistral_result)
|
||||||
|
|
||||||
|
def _node_get_capabilities(self, node):
|
||||||
|
"""Get node capabilities."""
|
||||||
|
return nodeutils.capabilities_to_dict(
|
||||||
|
node['properties'].get('capabilities'))
|
||||||
|
@@ -14,6 +14,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
import collections
|
import collections
|
||||||
import mock
|
import mock
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from mistral.workflow import utils as mistral_workflow_utils
|
from mistral.workflow import utils as mistral_workflow_utils
|
||||||
from oslo_concurrency.processutils import ProcessExecutionError
|
from oslo_concurrency.processutils import ProcessExecutionError
|
||||||
@@ -22,6 +23,7 @@ from tripleo_common.actions import validations
|
|||||||
from tripleo_common import constants
|
from tripleo_common import constants
|
||||||
from tripleo_common.tests import base
|
from tripleo_common.tests import base
|
||||||
from tripleo_common.tests.utils import test_validations
|
from tripleo_common.tests.utils import test_validations
|
||||||
|
from tripleo_common.utils import nodes as nodeutils
|
||||||
|
|
||||||
|
|
||||||
class GetPubkeyActionTest(base.TestCase):
|
class GetPubkeyActionTest(base.TestCase):
|
||||||
@@ -455,3 +457,202 @@ class TestCheckNodeBootConfigurationAction(base.TestCase):
|
|||||||
|
|
||||||
action = validations.CheckNodeBootConfigurationAction(**action_args)
|
action = validations.CheckNodeBootConfigurationAction(**action_args)
|
||||||
self.assertEqual(expected, action.run())
|
self.assertEqual(expected, action.run())
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerifyProfilesAction(base.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestVerifyProfilesAction, self).setUp()
|
||||||
|
|
||||||
|
self.nodes = []
|
||||||
|
self.flavors = {name: (self._get_fake_flavor(name), 1)
|
||||||
|
for name in ('compute', 'control')}
|
||||||
|
|
||||||
|
def _get_fake_node(self, profile=None, possible_profiles=[],
|
||||||
|
provision_state='available'):
|
||||||
|
caps = {'%s_profile' % p: '1'
|
||||||
|
for p in possible_profiles}
|
||||||
|
if profile is not None:
|
||||||
|
caps['profile'] = profile
|
||||||
|
caps = nodeutils.dict_to_capabilities(caps)
|
||||||
|
return {
|
||||||
|
'uuid': str(uuid4()),
|
||||||
|
'properties': {'capabilities': caps},
|
||||||
|
'provision_state': provision_state,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_fake_flavor(self, name, profile=''):
|
||||||
|
the_profile = profile or name
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'profile': the_profile,
|
||||||
|
'capabilities:boot_option': 'local',
|
||||||
|
'capabilities:profile': the_profile
|
||||||
|
}
|
||||||
|
|
||||||
|
def _test(self, expected_result):
|
||||||
|
action = validations.VerifyProfilesAction(self.nodes, self.flavors)
|
||||||
|
result = action.run()
|
||||||
|
|
||||||
|
self.assertEqual(expected_result, result)
|
||||||
|
|
||||||
|
def test_no_matching_without_scale(self):
|
||||||
|
self.flavors = {name: (object(), 0)
|
||||||
|
for name in self.flavors}
|
||||||
|
self.nodes[:] = [self._get_fake_node(profile='fake'),
|
||||||
|
self._get_fake_node(profile='fake')]
|
||||||
|
|
||||||
|
expected = mistral_workflow_utils.Result(
|
||||||
|
data={
|
||||||
|
'errors': [],
|
||||||
|
'warnings': [],
|
||||||
|
})
|
||||||
|
self._test(expected)
|
||||||
|
|
||||||
|
def test_exact_match(self):
|
||||||
|
self.nodes[:] = [self._get_fake_node(profile='compute'),
|
||||||
|
self._get_fake_node(profile='control')]
|
||||||
|
|
||||||
|
expected = mistral_workflow_utils.Result(
|
||||||
|
data={
|
||||||
|
'errors': [],
|
||||||
|
'warnings': [],
|
||||||
|
})
|
||||||
|
self._test(expected)
|
||||||
|
|
||||||
|
def test_nodes_with_no_profiles_present(self):
|
||||||
|
self.nodes[:] = [self._get_fake_node(profile='compute'),
|
||||||
|
self._get_fake_node(profile=None),
|
||||||
|
self._get_fake_node(profile='foobar'),
|
||||||
|
self._get_fake_node(profile='control')]
|
||||||
|
|
||||||
|
expected = mistral_workflow_utils.Result(
|
||||||
|
data={
|
||||||
|
'warnings': [
|
||||||
|
'There are 1 ironic nodes with no profile that will not '
|
||||||
|
'be used: %s' % self.nodes[1].get('uuid')
|
||||||
|
],
|
||||||
|
'errors': [],
|
||||||
|
})
|
||||||
|
self._test(expected)
|
||||||
|
|
||||||
|
def test_more_nodes_with_profiles_present(self):
|
||||||
|
self.nodes[:] = [self._get_fake_node(profile='compute'),
|
||||||
|
self._get_fake_node(profile='compute'),
|
||||||
|
self._get_fake_node(profile='compute'),
|
||||||
|
self._get_fake_node(profile='control')]
|
||||||
|
|
||||||
|
expected = mistral_workflow_utils.Result(
|
||||||
|
data={
|
||||||
|
'warnings': ["2 nodes with profile compute won't be used for "
|
||||||
|
"deployment now"],
|
||||||
|
'errors': [],
|
||||||
|
})
|
||||||
|
self._test(expected)
|
||||||
|
|
||||||
|
def test_no_nodes(self):
|
||||||
|
# One error per each flavor
|
||||||
|
expected = mistral_workflow_utils.Result(
|
||||||
|
error={'errors': ['Error: only 0 of 1 requested ironic nodes are '
|
||||||
|
'tagged to profile compute (for flavor '
|
||||||
|
'compute)\n'
|
||||||
|
'Recommendation: tag more nodes using openstack '
|
||||||
|
'baremetal node set --property '
|
||||||
|
'"capabilities=profile:compute,'
|
||||||
|
'boot_option:local" <NODE ID>',
|
||||||
|
'Error: only 0 of 1 requested ironic nodes are '
|
||||||
|
'tagged to profile control (for flavor '
|
||||||
|
'control).\n'
|
||||||
|
'Recommendation: tag more nodes using openstack '
|
||||||
|
'baremetal node set --property '
|
||||||
|
'"capabilities=profile:control,'
|
||||||
|
'boot_option:local" <NODE ID>'],
|
||||||
|
'warnings': []})
|
||||||
|
|
||||||
|
action = validations.VerifyProfilesAction(self.nodes, self.flavors)
|
||||||
|
result = action.run()
|
||||||
|
self.assertEqual(expected.error['errors'].sort(),
|
||||||
|
result.error['errors'].sort())
|
||||||
|
self.assertEqual(expected.error['warnings'], result.error['warnings'])
|
||||||
|
self.assertEqual(None, result.data)
|
||||||
|
|
||||||
|
def test_not_enough_nodes(self):
|
||||||
|
self.nodes[:] = [self._get_fake_node(profile='compute')]
|
||||||
|
expected = mistral_workflow_utils.Result(
|
||||||
|
error={'errors': ['Error: only 0 of 1 requested ironic nodes are '
|
||||||
|
'tagged to profile control (for flavor '
|
||||||
|
'control).\n'
|
||||||
|
'Recommendation: tag more nodes using openstack '
|
||||||
|
'baremetal node set --property '
|
||||||
|
'"capabilities=profile:control,'
|
||||||
|
'boot_option:local" <NODE ID>'],
|
||||||
|
'warnings': []})
|
||||||
|
self._test(expected)
|
||||||
|
|
||||||
|
def test_scale(self):
|
||||||
|
# active nodes with assigned profiles are fine
|
||||||
|
self.nodes[:] = [self._get_fake_node(profile='compute',
|
||||||
|
provision_state='active'),
|
||||||
|
self._get_fake_node(profile='control')]
|
||||||
|
|
||||||
|
expected = mistral_workflow_utils.Result(
|
||||||
|
data={
|
||||||
|
'errors': [],
|
||||||
|
'warnings': [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self._test(expected)
|
||||||
|
|
||||||
|
def test_assign_profiles_wrong_state(self):
|
||||||
|
# active nodes are not considered for assigning profiles
|
||||||
|
self.nodes[:] = [self._get_fake_node(possible_profiles=['compute'],
|
||||||
|
provision_state='active'),
|
||||||
|
self._get_fake_node(possible_profiles=['control'],
|
||||||
|
provision_state='cleaning'),
|
||||||
|
self._get_fake_node(profile='compute',
|
||||||
|
provision_state='error')]
|
||||||
|
expected = mistral_workflow_utils.Result(
|
||||||
|
error={
|
||||||
|
'warnings': [
|
||||||
|
'There are 1 ironic nodes with no profile that will not '
|
||||||
|
'be used: %s' % self.nodes[0].get('uuid')
|
||||||
|
],
|
||||||
|
'errors': [
|
||||||
|
'Error: only 0 of 1 requested ironic nodes are tagged to '
|
||||||
|
'profile control (for flavor control).\n'
|
||||||
|
'Recommendation: tag more nodes using openstack baremetal '
|
||||||
|
'node set --property "capabilities=profile:control,'
|
||||||
|
'boot_option:local" <NODE ID>',
|
||||||
|
'Error: only 0 of 1 requested ironic nodes are tagged to '
|
||||||
|
'profile compute (for flavor compute).\n'
|
||||||
|
'Recommendation: tag more nodes using openstack baremetal '
|
||||||
|
'node set --property "capabilities=profile:compute,'
|
||||||
|
'boot_option:local" <NODE ID>'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
action = validations.VerifyProfilesAction(self.nodes, self.flavors)
|
||||||
|
result = action.run()
|
||||||
|
self.assertEqual(expected.error['errors'].sort(),
|
||||||
|
result.error['errors'].sort())
|
||||||
|
self.assertEqual(expected.error['warnings'], result.error['warnings'])
|
||||||
|
self.assertEqual(None, result.data)
|
||||||
|
|
||||||
|
def test_no_spurious_warnings(self):
|
||||||
|
self.nodes[:] = [self._get_fake_node(profile=None)]
|
||||||
|
self.flavors = {'baremetal': (
|
||||||
|
self._get_fake_flavor('baremetal', None), 1)}
|
||||||
|
expected = mistral_workflow_utils.Result(
|
||||||
|
error={
|
||||||
|
'warnings': [
|
||||||
|
'There are 1 ironic nodes with no profile that will not '
|
||||||
|
'be used: %s' % self.nodes[0].get('uuid')
|
||||||
|
],
|
||||||
|
'errors': [
|
||||||
|
'Error: only 0 of 1 requested ironic nodes are tagged to '
|
||||||
|
'profile baremetal (for flavor baremetal).\n'
|
||||||
|
'Recommendation: tag more nodes using openstack baremetal '
|
||||||
|
'node set --property "capabilities=profile:baremetal,'
|
||||||
|
'boot_option:local" <NODE ID>'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
self._test(expected)
|
||||||
|
@@ -449,3 +449,71 @@ workflows:
|
|||||||
warnings: <% $.warnings %>
|
warnings: <% $.warnings %>
|
||||||
on-success:
|
on-success:
|
||||||
- fail: <% $.get('status') = "FAILED" %>
|
- fail: <% $.get('status') = "FAILED" %>
|
||||||
|
|
||||||
|
verify_profiles:
|
||||||
|
input:
|
||||||
|
- flavors: []
|
||||||
|
- run_validations: true
|
||||||
|
- queue_name: tripleo
|
||||||
|
output:
|
||||||
|
errors: <% $.errors %>
|
||||||
|
warnings: <% $.warnings %>
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
check_run_validations:
|
||||||
|
on-complete:
|
||||||
|
- get_ironic_nodes: <% $.run_validations %>
|
||||||
|
- send_message: <% not $.run_validations %>
|
||||||
|
|
||||||
|
get_ironic_nodes:
|
||||||
|
action: ironic.node_list
|
||||||
|
on-success: verify_profiles
|
||||||
|
on-error: failed_get_ironic_nodes
|
||||||
|
input:
|
||||||
|
maintenance: false
|
||||||
|
detail: true
|
||||||
|
publish:
|
||||||
|
nodes: <% task(get_ironic_nodes).result %>
|
||||||
|
|
||||||
|
failed_get_ironic_nodes:
|
||||||
|
on-success: send_message
|
||||||
|
publish:
|
||||||
|
status: FAILED
|
||||||
|
message: <% task(get_ironic_nodes).result %>
|
||||||
|
|
||||||
|
verify_profiles:
|
||||||
|
action: tripleo.validations.verify_profiles
|
||||||
|
input:
|
||||||
|
nodes: <% $.nodes %>
|
||||||
|
flavors: <% $.flavors %>
|
||||||
|
on-success: send_message
|
||||||
|
on-error: fail_verify_profiles
|
||||||
|
publish:
|
||||||
|
errors: <% task(verify_profiles).result.errors %>
|
||||||
|
warnings: <% task(verify_profiles).result.warnings %>
|
||||||
|
publish-on-error:
|
||||||
|
errors: <% task(verify_profiles).result.errors %>
|
||||||
|
warnings: <% task(verify_profiles).result.warnings %>
|
||||||
|
|
||||||
|
fail_verify_profiles:
|
||||||
|
on-success: send_message
|
||||||
|
publish:
|
||||||
|
status: Failed
|
||||||
|
message: <% task(verify_profiles).result %>
|
||||||
|
|
||||||
|
send_message:
|
||||||
|
action: zaqar.queue_post
|
||||||
|
retry: count=5 delay=1
|
||||||
|
input:
|
||||||
|
queue_name: <% $.queue_name %>
|
||||||
|
messages:
|
||||||
|
body:
|
||||||
|
type: tripleo.validations.v1.verify_profiles
|
||||||
|
payload:
|
||||||
|
status: <% $.get('status', 'SUCCESS') %>
|
||||||
|
message: <% $.get('message', '') %>
|
||||||
|
execution: <% execution() %>
|
||||||
|
errors: <% $.errors %>
|
||||||
|
warnings: <% $.warnings %>
|
||||||
|
on-success:
|
||||||
|
- fail: <% $.get('status') = "FAILED" %>
|
||||||
|
Reference in New Issue
Block a user