#!/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. """verify_profiles module Used by the collect_flavors_and_verify_profiles validation. """ from ansible.module_utils.basic import AnsibleModule # noqa from yaml import safe_load as yaml_safe_load DOCUMENTATION = ''' --- module: verify_profiles short_description: Check that profiles have enough nodes description: - Validate that the profiles assigned have enough nodes available. - Used by the collect_flavors_and_verify_profiles - Owned jointly by DFG Compute & DFG Hardware Provisioning 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 = int(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}" ') 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=yaml_safe_load(DOCUMENTATION)['options'] ) 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()