
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
215 lines
6.5 KiB
Python
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()
|