From 5dfd88a23e227c9824ab922ace01f08d6d40145a Mon Sep 17 00:00:00 2001 From: "Brad P. Crochet" Date: Wed, 18 Apr 2018 14:34:37 -0400 Subject: [PATCH] Add validation for verifying profiles Verify that profiles and flavors are matchy-matchy. Change-Id: Ib97fe8d31c90472f1c712bc21f16a7f04371136c Implements: blueprint workflow-move-validations --- ... collect-flavors-and-verify-profiles.yaml} | 8 +- validations/library/verify_profiles.py | 167 ++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) rename validations/{collect-flavors.yaml => collect-flavors-and-verify-profiles.yaml} (65%) create mode 100644 validations/library/verify_profiles.py diff --git a/validations/collect-flavors.yaml b/validations/collect-flavors-and-verify-profiles.yaml similarity index 65% rename from validations/collect-flavors.yaml rename to validations/collect-flavors-and-verify-profiles.yaml index 21152de45..a90ad5b46 100644 --- a/validations/collect-flavors.yaml +++ b/validations/collect-flavors-and-verify-profiles.yaml @@ -10,7 +10,13 @@ - pre-deployment - pre-upgrade tasks: - - name: Check the flavors + - name: Collect and check the flavors check_flavors: roles_info: "{{ lookup('roles_info', wantlist=True) }}" flavors: "{{ lookup('nova_flavors', wantlist=True) }}" + register: flavor_result + + - name: Verify the profiles + verify_profiles: + nodes: "{{ lookup('ironic_nodes', wantlist=True) }}" + flavors: "{{ flavor_result.flavors }}" diff --git a/validations/library/verify_profiles.py b/validations/library/verify_profiles.py new file mode 100644 index 000000000..ef8c5748b --- /dev/null +++ b/validations/library/verify_profiles.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# Copyright 2018 Red Hat, Inc. +# 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 ansible.module_utils.basic import AnsibleModule # noqa + +DOCUMENTATION = ''' +--- +module: verify_profiles +short_description: Check that profiles have enough nodes +description: + - Validate that the profiles assigned have enough nodes available. +options: + nodes: + required: true + description: + - A list of nodes + type: list + flavors: + required: true + description: + - A dictionary of flavors + type: dict + +author: "Brad P. Crochet" +''' + +EXAMPLES = ''' +- hosts: undercloud + tasks: + - name: Collect the flavors + check_flavors: + roles_info: "{{ lookup('roles_info', wantlist=True) }}" + flavors: "{{ lookup('nova_flavors', wantlist=True) }}" + register: flavor_result + - name: Check the profiles + verify_profiles: + nodes: "{{ lookup('ironic_nodes', wantlist=True) }}" + flavors: flavor_result.flavors +''' + + +def _capabilities_to_dict(caps): + """Convert the Node's capabilities into a dictionary.""" + if not caps: + return {} + if isinstance(caps, dict): + return caps + return dict([key.split(':', 1) for key in caps.split(',')]) + + +def _node_get_capabilities(node): + """Get node capabilities.""" + return _capabilities_to_dict( + node['properties'].get('capabilities')) + + +def verify_profiles(nodes, flavors): + """Check if roles info is correct + + :param nodes: list of nodes + :param flavors: dictionary of flavors + :returns warnings: List of warning messages + errors: List of error messages + """ + errors = [] + warnings = [] + + bm_nodes = {node['uuid']: node for node in nodes + if node['provision_state'] in ('available', 'active')} + + free_node_caps = {uu: _node_get_capabilities(node) + for uu, node in bm_nodes.items()} + + profile_flavor_used = False + for flavor_name, (flavor, scale) in flavors.items(): + if not scale: + continue + + profile = None + keys = flavor.get('keys') + if keys: + profile = keys.get('capabilities:profile') + + if not profile and len(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" ') + 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 warnings, errors + + +def main(): + module = AnsibleModule(argument_spec=dict( + nodes=dict(required=True, type='list'), + flavors=dict(required=True, type='dict') + )) + + nodes = module.params.get('nodes') + flavors = module.params.get('flavors') + + warnings, errors = verify_profiles(nodes, + flavors) + + if errors: + module.fail_json(msg="\n".join(errors)) + elif warnings: + module.exit_json(warnings="\n".join(warnings)) + else: + module.exit_json( + msg="No profile errors detected.") + + +if __name__ == '__main__': + main()