08533b45c3
This change modifies the argument_spec making it so we load the options key from the documentation constant. This will reduce the code we have to maintain and ensure our documentation is always in sync with the module capabilities. Change-Id: Iaf94b6300a58de4367b4c9f2c83cc112e7c5361a Co-Authored-by: Kevin Carter <kecarter@redhat.com> Signed-off-by: Gael Chamoulaud <gchamoul@redhat.com>
168 lines
5.3 KiB
Python
168 lines
5.3 KiB
Python
#!/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
|
|
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.
|
|
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}" <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 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()
|