Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .ansible-lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
# Red Hat Ansible Collection Certification .ansible-lint file
# Based on:
# https://github.com/ansible-collections/partner-certification-requirements
profile: production

exclude_paths:
- tests/integration
- tests/unit
- extensions/molecule
- changelogs
- docs
- .ansible
- .github

warn_list:
- no-log-password
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ The Everpure FlashArray collection consists of the latest versions of the FlashA
- some modules require higher versions of Purity
- Some modules require specific Purity versions
- distro
- py-pure-client >= 1.75.0
- py-pure-client >= 1.82.0
- python >= 3.9
- netaddr >= 1.2.0
- requests
- pycountry
- packaging
- pyz
- pytz
- urllib3

## Installation
Expand Down
2 changes: 2 additions & 0 deletions changelogs/fragments/1027_purefa_host_initiators.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bugfixes:
- purefa_host - Reconcile a host's WWNs, IQNs and NQNs by adding only missing initiators and removing only unwanted ones, so one can be dropped by omitting it and those that stay are untouched
4 changes: 4 additions & 0 deletions changelogs/fragments/1027_purefa_info_host_dict.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
minor_changes:
- purefa_info - Add ``logged_in_ports`` to each host in the ``hosts`` subset, mapping every host WWN/IQN/NQN to the array target ports it is currently logged into
bugfixes:
- purefa_info - Fix TypeError gathering the ``hosts`` subset for hosts that have preferred arrays set, as ``preferred_arrays`` is a list of references not a dict
15 changes: 15 additions & 0 deletions galaxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@ dependencies: {}
repository: https://github.com/Everpure-Ansible/FlashArray-Collection
documentation: https://github.com/Everpure-Ansible/FlashArray-Collection
issues: https://github.com/Everpure-Ansible/FlashArray-Collection/issues
build_ignore:
- tests/integration
- tests/unit
- extensions/molecule
- changelogs
- docs
- collections
- .github
- .ansible
- .tox
- .venv
- .vscode
- .pytest_cache
- .pre-commit-config.yaml
- settings.json
176 changes: 77 additions & 99 deletions plugins/modules/purefa_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,23 @@
elements: str
description:
- List of wwns of the host.
- This is the complete desired set of WWNs. The host is reconciled to match
it, adding any that are missing and removing any that are not listed, so a
single WWN can be removed by simply omitting it from the list.
iqn:
type: list
elements: str
description:
- List of IQNs of the host.
- This is the complete desired set of IQNs; the host is reconciled to match
it, adding and removing IQNs as needed.
nqn:
type: list
elements: str
description:
- List of NQNs of the host. Note that NMVe hosts can only possess NQNs.
- This is the complete desired set of NQNs; the host is reconciled to match
it, adding and removing NQNs as needed.
Multi-protocol is not allowed for these hosts.
volume:
type: str
Expand Down Expand Up @@ -445,113 +452,84 @@ def _set_host_initiators(module, array):
)


def _sync_initiators(module, array, add_field, remove_field, desired, current, error):
"""Reconcile a single initiator type to the desired set.

The FlashArray ``patch_hosts`` API exposes three fields per initiator type:
``wwns``/``iqns``/``nqns`` *replace* the whole associated list, while
``add_*`` associates extra initiators and ``remove_*`` disassociates
specific ones. We use the ``add_*``/``remove_*`` pair (never the replacing
field) so that only the genuinely changed initiators are touched: an
unwanted WWN/IQN/NQN is dropped by omitting it from the list, and the
initiators that stay are left untouched rather than being removed and
re-added (which would needlessly disrupt their existing sessions).

Returns True if a change was (or would be) made.
"""
to_add = [item for item in desired if item not in current]
to_remove = [item for item in current if item not in desired]
if not to_add and not to_remove:
return False
if not module.check_mode:
patch = {}
if to_add:
patch[add_field] = to_add
if to_remove:
patch[remove_field] = to_remove
res = get_with_context(
array,
"patch_hosts",
CONTEXT_API_VERSION,
module,
names=[module.params["name"]],
host=HostPatch(**patch),
)
check_response(res, module, error)
return True


def _update_host_initiators(module, array, answer=False):
"""Change host initiator if iscsi or nvme or add new FC WWNs"""
"""Reconcile host initiators (iSCSI, NVMe and FC) to the requested set"""
current_connectors = get_host(module, array)
if module.params["nqn"]:
if module.params["nqn"] != [""]:
if sorted(current_connectors.nqns) != sorted(module.params["nqn"]):
answer = True
if not module.check_mode:
res = get_with_context(
array,
"patch_hosts",
CONTEXT_API_VERSION,
module,
names=[module.params["name"]],
host=HostPatch(nqns=module.params["nqn"]),
)
check_response(
res,
module,
f"Change of NVMe NQNs failed on host {module.params['name']}",
)
elif current_connectors.nqns:
desired = [] if module.params["nqn"] == [""] else module.params["nqn"]
if _sync_initiators(
module,
array,
"add_nqns",
"remove_nqns",
desired,
current_connectors.nqns or [],
f"Change of NVMe NQNs failed on host {module.params['name']}",
):
answer = True
if not module.check_mode:
res = get_with_context(
array,
"patch_hosts",
CONTEXT_API_VERSION,
module,
names=[module.params["name"]],
host=HostPatch(remove_nqns=current_connectors.nqns),
)
check_response(
res,
module,
f"Removal of NVMe NQNs failed on host {module.params['name']}",
)
if module.params["iqn"]:
if module.params["iqn"] != [""]:
if sorted(current_connectors.iqns) != sorted(module.params["iqn"]):
answer = True
if not module.check_mode:
res = get_with_context(
array,
"patch_hosts",
CONTEXT_API_VERSION,
module,
names=[module.params["name"]],
host=HostPatch(iqns=module.params["iqn"]),
)
check_response(
res,
module,
f"Change of iSCSI IQNs failed on host {module.params['name']}",
)
elif current_connectors.iqns:
desired = [] if module.params["iqn"] == [""] else module.params["iqn"]
if _sync_initiators(
module,
array,
"add_iqns",
"remove_iqns",
desired,
current_connectors.iqns or [],
f"Change of iSCSI IQNs failed on host {module.params['name']}",
):
answer = True
if not module.check_mode:
res = get_with_context(
array,
"patch_hosts",
CONTEXT_API_VERSION,
module,
names=[module.params["name"]],
host=HostPatch(remove_iqns=current_connectors.iqns),
)
check_response(
res,
module,
f"Removal of iSCSI IQNs failed on host {module.params['name']}",
)
if module.params["wwns"]:
module.params["wwns"] = [wwn.replace(":", "") for wwn in module.params["wwns"]]
module.params["wwns"] = [wwn.upper() for wwn in module.params["wwns"]]
if module.params["wwns"] != [""]:
if sorted(current_connectors.wwns) != sorted(module.params["wwns"]):
answer = True
if not module.check_mode:
res = get_with_context(
array,
"patch_hosts",
CONTEXT_API_VERSION,
module,
names=module.params["name"],
host=HostPatch(wwns=module.params["wwns"]),
)
check_response(
res,
module,
f"Change of FC WWNs failed on host {module.params['name']}",
)
elif current_connectors.wwns:
module.params["wwns"] = [
wwn.replace(":", "").upper() for wwn in module.params["wwns"]
]
desired = [] if module.params["wwns"] == [""] else module.params["wwns"]
if _sync_initiators(
module,
array,
"add_wwns",
"remove_wwns",
desired,
current_connectors.wwns or [],
f"Change of FC WWNs failed on host {module.params['name']}",
):
answer = True
if not module.check_mode:
res = get_with_context(
array,
"patch_hosts",
CONTEXT_API_VERSION,
module,
names=[module.params["name"]],
host=HostPatch(remove_wwns=current_connectors.wwns),
)
check_response(
res,
module,
f"Removal of FC WWNs failed on host {module.params['name']}",
)
return answer


Expand Down
33 changes: 30 additions & 3 deletions plugins/modules/purefa_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -1890,6 +1890,22 @@ def generate_host_dict(array, performance):
hosts_balance = list(array.get_hosts_performance_balance().items)
if performance:
hosts_performance = list(array.get_hosts_performance().items)
# Build a live login map (initiator identifier -> set of array target port
# names) so each host WWN/IQN/NQN can report exactly where it is logged in.
login_map = {}
for login in list(array.get_ports_initiators().items):
target_name = getattr(login.target, "name", None)
if not target_name:
continue
initiator = login.initiator
init_wwn = getattr(initiator, "wwn", None)
for key in (
init_wwn.replace(":", "").upper() if init_wwn else None,
getattr(initiator, "iqn", None),
getattr(initiator, "nqn", None),
):
if key:
login_map.setdefault(key, set()).add(target_name)
for host in hosts:
hostname = host.name
host_info[hostname] = {
Expand All @@ -1901,6 +1917,7 @@ def generate_host_dict(array, performance):
"host_user": getattr(host.chap, "host_user", None),
"target_user": getattr(host.chap, "target_user", None),
"target_port": [],
"logged_in_ports": {},
"volumes": [],
"tags": [],
"performance": [],
Expand All @@ -1910,6 +1927,17 @@ def generate_host_dict(array, performance):
"time_remaining": getattr(host, "time_remaining", None),
"vlan": getattr(host, "vlan", None),
}
# Report which array target ports each host initiator is logged into.
# An empty list means the identifier is not logged into any port, so
# the WWN/IQN/NQN can be safely removed.
logged_in = {}
for wwn in getattr(host, "wwns", None) or []:
logged_in[wwn] = sorted(login_map.get(wwn.replace(":", "").upper(), []))
for iqn in getattr(host, "iqns", None) or []:
logged_in[iqn] = sorted(login_map.get(iqn, []))
for nqn in getattr(host, "nqns", None) or []:
logged_in[nqn] = sorted(login_map.get(nqn, []))
host_info[hostname]["logged_in_ports"] = logged_in
host_connections = list(array.get_connections(host_names=[hostname]).items)
for connection in host_connections:
connection_dict = {
Expand All @@ -1920,9 +1948,8 @@ def generate_host_dict(array, performance):
}
host_info[hostname]["volumes"].append(connection_dict)
for pref_array in host.preferred_arrays:
host_info[hostname]["preferred_array"].append(
host.preferred_arrays[pref_array].name
)
# preferred_arrays is a list of Reference objects, not a dict
host_info[hostname]["preferred_array"].append(pref_array.name)

if host.is_local:
host_info[host.name]["port_connectivity"] = host.port_connectivity.details
Expand Down
Loading
Loading