Will Szumski 5f1ffe2b99 Handle aggregate host list set to None
A freshly created host aggregate can have the host list set to None,
consequently you'd hit:

```
failed: [localhost] (item={'name': 'gpu', 'hosts': [], 'metadata': {'type': 'gpu'}}) => {"ansible_loop_var": "item", "changed": false, "item": {"hosts": [], "metadata": {"type": "gpu"}, "name": "gpu"}, "module_stderr": "Traceback (most recent call last):\n  File \"/var/lib/home/stackhpc/.ansible/tmp/ansible-tmp-1642696576.6728637-1456290-187052400642084/Ansiba
llZ_host_aggregate.py\", line 100, in <module>\n    _ansiballz_main()\n  File \"/var/lib/home/stackhpc/.ansible/tmp/ansible-tmp-1642696576.6728637-1456290-187052400642084/AnsiballZ_host_aggregate.py\", line 92, in _ansiballz_main\n    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)\n  File \"/var/lib/home/stackhpc/.ansible/tmp/ansible-tmp-1642696576.672
8637-1456290-187052400642084/AnsiballZ_host_aggregate.py\", line 41, in invoke_module\n    run_name='__main__', alter_sys=True)\n  File \"/usr/lib64/python3.6/runpy.py\", line 205, in run_module\n    return _run_module_code(code, init_globals, run_name, mod_spec)\n  File \"/usr/lib64/python3.6/runpy.py\", line 96, in _run_module_code\n    mod_name, mod_spec, p
kg_name, script_name)\n  File \"/usr/lib64/python3.6/runpy.py\", line 85, in _run_code\n    exec(code, run_globals)\n  File \"/tmp/ansible_os_nova_host_aggregate_payload_qwjtdtjj/ansible_os_nova_host_aggregate_payload.zip/ansible_collections/openstack/cloud/plugins/modules/host_aggregate.py\", line 214, in <module>\n  File \"/tmp/ansible_os_nova_host_aggregate
_payload_qwjtdtjj/ansible_os_nova_host_aggregate_payload.zip/ansible_collections/openstack/cloud/plugins/modules/host_aggregate.py\", line 210, in main\n  File \"/tmp/ansible_os_nova_host_aggregate_payload_qwjtdtjj/ansible_os_nova_host_aggregate_payload.zip/ansible_collections/openstack/cloud/plugins/module_utils/openstack.py\", line 407, in __call__\n  File \
"/tmp/ansible_os_nova_host_aggregate_payload_qwjtdtjj/ansible_os_nova_host_aggregate_payload.zip/ansible_collections/openstack/cloud/plugins/modules/host_aggregate.py\", line 176, in run\n  File \"/tmp/ansible_os_nova_host_aggregate_payload_qwjtdtjj/ansible_os_nova_host_aggregate_payload.zip/ansible_collections/openstack/cloud/plugins/modules/host_aggregate.py
\", line 138, in _update_hosts\nTypeError: 'NoneType' object is not iterable\n", "module_stdout": "", "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error", "rc": 1}
```

I've not investigated which API and library combinations elict this
behaviour, but it does seem to occur. We can safely handle this
possibility in backwards compatible way.

It is possible to workaround this issue by invoking the module a second
time.

Change-Id: Ie14391f18c0f65833d00a4b4f6b1b314a0903d2b
2022-03-17 19:36:38 +00:00

215 lines
6.5 KiB
Python

#!/usr/bin/python
# Copyright 2016 Jakub Jursa <jakub.jursa1@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = '''
---
module: host_aggregate
short_description: Manage OpenStack host aggregates
author: OpenStack Ansible SIG
description:
- Create, update, or delete OpenStack host aggregates. If a aggregate
with the supplied name already exists, it will be updated with the
new name, new availability zone, new metadata and new list of hosts.
options:
name:
description: Name of the aggregate.
required: true
type: str
metadata:
description: Metadata dict.
type: dict
availability_zone:
description: Availability zone to create aggregate into.
type: str
hosts:
description: List of hosts to set for an aggregate.
type: list
elements: str
purge_hosts:
description: Whether hosts not in I(hosts) should be removed from the aggregate
type: bool
default: true
state:
description: Should the resource be present or absent.
choices: [present, absent]
default: present
type: str
requirements:
- "python >= 3.6"
- "openstacksdk"
extends_documentation_fragment:
- openstack.cloud.openstack
'''
EXAMPLES = '''
# Create a host aggregate
- openstack.cloud.host_aggregate:
cloud: mycloud
state: present
name: db_aggregate
hosts:
- host1
- host2
metadata:
type: dbcluster
# Add an additional host to the aggregate
- openstack.cloud.host_aggregate:
cloud: mycloud
state: present
name: db_aggregate
hosts:
- host3
purge_hosts: false
metadata:
type: dbcluster
# Delete an aggregate
- openstack.cloud.host_aggregate:
cloud: mycloud
state: absent
name: db_aggregate
'''
RETURN = '''
'''
from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
class ComputeHostAggregateModule(OpenStackModule):
argument_spec = dict(
name=dict(required=True),
metadata=dict(required=False, default=None, type='dict'),
availability_zone=dict(required=False, default=None),
hosts=dict(required=False, default=None, type='list', elements='str'),
purge_hosts=dict(default=True, type='bool'),
state=dict(default='present', choices=['absent', 'present']),
)
module_kwargs = dict(
supports_check_mode=True
)
def _needs_update(self, aggregate):
new_metadata = (self.params['metadata'] or {})
if self.params['availability_zone'] is not None:
new_metadata['availability_zone'] = self.params['availability_zone']
if self.params['name'] != aggregate.name:
return True
if self.params['hosts'] is not None:
if self.params['purge_hosts']:
if set(self.params['hosts']) != set(aggregate.hosts):
return True
else:
intersection = set(self.params['hosts']).intersection(set(aggregate.hosts))
if set(self.params['hosts']) != intersection:
return True
if self.params['availability_zone'] is not None:
if self.params['availability_zone'] != aggregate.availability_zone:
return True
if self.params['metadata'] is not None:
if new_metadata != aggregate.metadata:
return True
return False
def _system_state_change(self, aggregate):
state = self.params['state']
if state == 'absent' and aggregate:
return True
if state == 'present':
if aggregate is None:
return True
return self._needs_update(aggregate)
return False
def _update_hosts(self, aggregate, hosts, purge_hosts):
if hosts is None:
return
hosts_to_add = set(hosts) - set(aggregate.get("hosts") or [])
for i in hosts_to_add:
self.conn.add_host_to_aggregate(aggregate.id, i)
if not purge_hosts:
return
hosts_to_remove = set(aggregate.get("hosts") or []) - set(hosts)
for i in hosts_to_remove:
self.conn.remove_host_from_aggregate(aggregate.id, i)
def run(self):
name = self.params['name']
metadata = self.params['metadata']
availability_zone = self.params['availability_zone']
hosts = self.params['hosts']
purge_hosts = self.params['purge_hosts']
state = self.params['state']
if metadata is not None:
metadata.pop('availability_zone', None)
aggregates = self.conn.search_aggregates(name_or_id=name)
if len(aggregates) == 1:
aggregate = aggregates[0]
elif len(aggregates) == 0:
aggregate = None
else:
raise Exception("Should not happen")
if self.ansible.check_mode:
self.exit_json(changed=self._system_state_change(aggregate))
if state == 'present':
if aggregate is None:
aggregate = self.conn.create_aggregate(
name=name, availability_zone=availability_zone)
self._update_hosts(aggregate, hosts, False)
if metadata:
self.conn.set_aggregate_metadata(aggregate.id, metadata)
changed = True
else:
if self._needs_update(aggregate):
if availability_zone is not None:
aggregate = self.conn.update_aggregate(
aggregate.id, name=name,
availability_zone=availability_zone)
if metadata is not None:
metas = metadata
for i in (set(aggregate.metadata.keys()) - set(metadata.keys())):
if i != 'availability_zone':
metas[i] = None
self.conn.set_aggregate_metadata(aggregate.id, metas)
self._update_hosts(aggregate, hosts, purge_hosts)
changed = True
else:
changed = False
self.exit_json(changed=changed)
elif state == 'absent':
if aggregate is None:
changed = False
else:
self._update_hosts(aggregate, [], True)
self.conn.delete_aggregate(aggregate.id)
changed = True
self.exit_json(changed=changed)
def main():
module = ComputeHostAggregateModule()
module()
if __name__ == '__main__':
main()