Event-Driven Ansible for standalone Everpure FlashArrays.
A FlashArray already exposes the three things an event-driven loop needs:
a cursorable alert log, an attributable audit log, and its own performance
and capacity history. This project turns those into ansible-rulebook
events and acts on them through the everpure.flasharray collection. No
Pure1, no Fusion and no external monitoring stack are required; a
Prometheus/Alertmanager path exists for sites that already run one.
Status: a prototype, not a supported product. The rulebooks, the platform seed, the orchestrator workflows and the Slack bot are exercised against two Purity//FA 6.10.6 arrays, an Ansible Automation Platform 2.7 installation, a live Prometheus and a live automation orchestrator (the upstream code of the 2026.8 release). docs/VERIFICATION.md is the ledger of what is proven on hardware and what is replayed from fixtures, docs/DESIGN.md the design and scope, and docs/HARDWARE_NOTES.md the array, SDK and rulebook behaviour the rules are written around.
Three transports feed the same rulebooks and the same playbooks:
flowchart LR
subgraph fa["FlashArray"]
rest["REST API<br/>alerts, audits, hardware, space,<br/>performance, replication, config"]
slog["syslog<br/>purity.alert / purity.audit / purity.test"]
om["OpenMetrics exporter<br/>(Purity 6.7+)"]
end
subgraph rb["ansible-rulebook"]
events["purefa_events<br/>(pull, six signals)"]
pes["purefa_syslog<br/>(push, UDP/TCP)"]
am["ansible.eda.alertmanager"]
rules["rulebooks<br/>gated by maintenance windows,<br/>automation_users and per-site switches"]
end
prom["Prometheus rules<br/>+ Alertmanager"]
pb["playbooks<br/>everpure.flasharray modules"]
rest -->|poll| events --> rules
slog -->|UDP/TCP| pes --> rules
om -->|scrape| prom -->|webhook| am --> rules
rules -->|run_playbook| pb -->|"revert, light, cap, snapshot, ask"| fa
Requirements:
- Python 3.9+ and Java 17+ (for ansible-rulebook).
- ansible-rulebook 1.3.x and ansible-core 2.17, the tested versions;
requirements.txtholds the minimums. everpure.flasharray>= 1.46.0 andansible.eda>= 2.13.0 (requirements.yml).- py-pure-client >= 1.94.1.
- Purity//FA 6.10.6 (REST 2.46) is the tested array release.
python3 -m venv venv && . venv/bin/activate
make deps # pip install -r requirements.txt; ansible-galaxy collection install -r requirements.yml; make collection
cp env.example ~/.flasharray-eda.env # URLs and tokens for both arrays, APPROVAL_TOKEN, GIT_WEBHOOK_SECRET; optional AO_CLIENT_*, SLACK_WEBHOOK_URL; chmod 600
cp vars/example.yml vars/local-lab.yml # edit: arrays, absolute paths (alert_log, request_dir, baseline_dir), baselines, thresholds, switches
cp inventory/example.yml inventory/local-lab.yml
./run.sh extensions/eda/rulebooks/alerts.yml -v
The repository is the collection sdodsley.flasharray_eda (galaxy.yml at
the root). The rulebooks name their event sources by FQCN,
sdodsley.flasharray_eda.purefa_events and
sdodsley.flasharray_eda.purefa_syslog, and ansible-rulebook resolves them
from an installed collection. make collection (the last step of
make deps) builds the tarball into build/ and installs it under
./collections; docs/PACKAGING.md has the details.
run.sh sources env.sh, which prepares the environment:
- loads
~/.flasharray-eda.env(override withFAEDA_ENV); - activates
./venv(FAEDA_VENV), or else expectsansible-rulebookon the PATH and says so; - puts
./collections(when that directory exists) and~/.ansible/collectionsonANSIBLE_COLLECTIONS_PATH, which is where the FQCN sources resolve from; - defaults
FAEDA_VARStovars/local-lab.ymlandFAEDA_INVENTORYtoinventory/local-lab.yml.
run.sh then starts the rulebook. It passes
--source-dir extensions/eda/plugins/event_source, which ansible-rulebook
consults for bare names only and which is harmless for a FQCN, and it
hands APPROVAL_TOKEN and GIT_WEBHOOK_SECRET to ansible-rulebook with
--env-vars when they are set in the environment. That is how the webhook
rulesets read them: {{ APPROVAL_TOKEN }} in a rulebook is an environment
variable, not a vars-file key.
Three things to get right on the first run:
- Arrays. The example vars and inventory name two arrays. Either fill in
both the
ARRAY_A_*andARRAY_B_*pairs in the env file, or removearray-bfrompurefa_arrays, from the inventory and from thedr_drillsentry. The source plugin refuses to start when a listed array's environment variables are empty. - Paths.
alert_logandrequest_dirdefault under/var, which needs root or a directory created and chowned beforehand; for a local run point them at a directory the rulebook user can write.baseline_diris a separate checkout of the baseline repository (baseline/README.md), not this tree. The/varand/optdefaults are the decision-environment image layout. - Credentials. Tokens and URLs reach the plugin through
api_token_env/fa_url_envand the playbooks through the inventory'slookup('env', ...), so no vars file holds a secret. Give the polling token a read-only array role; the plugin only issues GETs. The remediation playbooks use the inventory credential, which should be a dedicated service account named inautomation_users.
| Rulebook | Source | Fires on | Action |
|---|---|---|---|
alerts.yml |
alerts | alert opened, reopened or raised in severity; alert resolved (closing) |
routes through the alert-code runbook map: notify, ticket, or per-code tasks, with the array's own action text and KB link |
change_control.yml |
audits, webhook | a governed setting changed by anyone but the automation account; a destructive command by a human; a signed git push | reconcile from the git baseline (NTP, DNS) and report who; notify; apply the baseline forward |
config_drift.yml |
config | NTP, DNS, LDAP, SafeMode/eradication, admin auth settings vs baseline (poll-based fallback) | NTP and DNS reapplied; the rest notify |
health.yml |
health | hardware component not ok; drive not healthy; controller failover; maintenance window; remote assist | hardware component not ok: identify LED + notify, cleared again when it recovers, logged only inside a maintenance window; drive not healthy, controller failover: notify; maintenance window, remote assist: logged |
replication.yml |
replication, tick | connection down or paths failed; replica link paused, unhealthy, lag over RPO (escalates after N polls); stretched pod member offline / recovered; protection group overdue or stuck; a tick (standalone; on the platform the seed's nightly schedule) | notify; move host preferred arrays to the online side and back; replicate now (with orchestrator_enabled, an approval in the orchestrator first); DR drill (copy a volume from the latest replicated snapshot on the target, verify, destroy) |
capacity.yml |
telemetry, webhook | used % over threshold; days-to-full from the array's 7-day history; an approval (bearer token) | approval request with request id; reclaim pending-eradication objects only when approved |
performance.yml |
telemetry | noisy neighbour on a hot array; capped volume back to normal; array write latency vs the same half hour last week | temporary IOPS cap with an eda/qos_cap tag on the array (a member of a volume group with its own QoS: notify, or cap the group, by qos_cap_scope); cap lifted; notify |
anomaly.yml |
telemetry + audits | data reduction drop + snapshot or write surge + a human destroying snapshots, all on one array inside 15 minutes | snapshot every local protection group now, lengthen the eradication delay, page |
syslog.yml |
purefa_syslog | purity.audit / purity.alert / purity.test lines pushed by the array |
fast notification seconds after the fact; a test line (the weekly self-check's) is recorded on receipt |
alertmanager.yml |
ansible.eda.alertmanager | alerts from prometheus/flasharray.rules.yml on the native OpenMetrics exporter |
the same playbooks, no custom code on the array side |
Gates decide what each event allows:
maintenance_active(one cached/maintenance-windowsread per array) travels on alert, audit, hardware and replication events. It holds back the alert runbooks, the reconcile, the identify LED, the preferred-array move and replicate-now. Config and telemetry events carry no such flag, so the NTP and DNS reapply and the QoS cap run inside a window too.automation_userskeeps the audit-driven rules (reconcile, protect-now) from reacting to the automation's own changes.- Three per-site switches fall back to notify:
qos_auto_cap,activecluster_auto_preferandreplication_auto_replicate_now. The reapply, the reconcile, the identify LED, protect-now and the DR drill have no switch. orchestrator_enabledmoves the capacity and overdue-replication decisions into the automation orchestrator.
Every record a playbook writes to the alert log carries time, kind,
severity and array. Every notification also reaches Slack when a
webhook is configured ("Notifications" below).
galaxy.yml, meta/runtime.yml the repository root is the collection sdodsley.flasharray_eda (docs/PACKAGING.md)
extensions/eda/plugins/event_source/ purefa_events.py (REST pull, six signals), purefa_syslog.py (push)
extensions/eda/rulebooks/ one rulebook per domain (table above), sources named by FQCN
playbooks/ notify, remediate_*, reconcile, apply_baseline, identify_led, replicate_now, dr_promote, dr_demote,
preferred_array, dr_drill, dr_switchover, alertmanager_silence, capacity_request/reclaim, qos_cap, protect_now,
expand_volume, orchestrator_post, self_check, definitions_check,
alert_runbook (+ runbooks/<code>.yml, included relative to the playbook)
orchestrator/ workflow definitions for the automation orchestrator and job_templates.txt
(orchestrator/README.md)
tools/ao_lint.py offline linter for those definitions (`make lint-ao`)
tools/ao_sync.py imports the definitions into an orchestrator (orchestrator/README.md, "Importing")
tools/activation_audit.py checks every FlashArray activation's extra vars against the vars files and,
with --recreate, recreates the drifted ones (aap/README.md, "Checking the activations")
slack/approval_bot.py Slack Socket Mode bot: approval cards with Approve/Reject buttons for the orchestrator
and the controller (slack/README.md)
aap/ seed.yml, vars, inventory and credential types that build the AAP 2.7 objects
(credentials, templates, workflow, schedules, activations);
execution and decision environment definitions (aap/README.md)
baseline/ schema of the git baseline (README); the real files live in a separate checkout
prometheus/ flasharray.rules.yml (alert rules for the native exporter, for the Prometheus
server), prometheus.example.yml and alertmanager.example.yml (scrape and routing)
decision-environment/ Containerfile and ansible-builder definition on ansible-rulebook v1.3.2; both build and
install the collection (see below)
vars/example.yml, inventory/example.yml copy to vars/local-<site>.yml and inventory/local-<site>.yml (gitignored)
env.example, env.sh, run.sh env file template, shared environment setup, launcher
tests/ unit/ (no array needed), mock_orchestrator.py, lab induction playbooks, fixtures,
replay rulebooks, e2e_*.sh (everything they write goes under tests/out/)
docs/ DESIGN.md (design and scope), VERIFICATION.md (what is exercised on hardware),
HARDWARE_NOTES.md (array and rulebook behaviour), ORCHESTRATOR.md (the handoff
contract), PACKAGING.md (the collection, its build and the decision environment)
Makefile `make check` runs what CI runs; `make collection` builds and installs the
collection; `make de` builds the image
requirements.txt, requirements.yml Python packages and the two collections (everpure.flasharray >= 1.46.0)
purefa_events polls one or more arrays with py-pure-client and emits plain
fact events; policy lives in the rulebooks. arrays is a list, or a JSON
string that decodes to one, so a platform credential can inject it as a
single variable. One instance runs several signals, each on its own
interval, and every REST call is bounded by request_timeout (default
30 s):
| Signal | Endpoints | Events |
|---|---|---|
| alerts | /alerts (state != closed), diffed locally |
purefa_alert with transition initial / opened / updated / reopened / closing / closed |
| audits | /audits newest N, cut at a local time cursor |
purefa_audit with user, command, subcommand, arguments, by_automation |
| health | /hardware, /drives, /controllers, /maintenance-windows, /support |
purefa_health, purefa_hardware per issue, purefa_controller_failover |
| telemetry | /arrays/space (+ 7-day history), /volumes/space (growth tracked across the growth_volumes, default 500, largest live volumes), /arrays/performance (+ same half hour last week), /volumes/performance top N (plus any volume tagged eda/qos_cap, whatever its rank), volume QoS and tags |
purefa_capacity (days_to_full, top growers, pending eradication, drr_drop, snapshot_surge), purefa_array_performance (baseline ratios, write_surge), purefa_volume_performance (streak counters, iops share, QoS limit, eda_capped) |
| replication | /array-connections (+ paths), /pod-replica-links, /pods (stretched), /protection-groups (+ targets, transfers) |
purefa_array_connection (failed paths), purefa_replica_link (rpo_breached, breach streak, paused_for_s), purefa_stretched_pod (online/offline arrays, recovered), purefa_pg_replication (overdue, stuck) |
| config | /arrays, /dns, /directory-services, /admins/settings, /syslog-servers, /smtp-servers, /alert-watchers, /support |
purefa_config |
Every event carries array; alert, audit, hardware and replication events
also carry maintenance_active. A poll failure arrives as
purefa_poll_error and polling continues. The plugin docstrings hold the
full argument and field lists.
purefa_syslog listens on UDP and TCP for the array's syslog and emits
purefa_syslog with the audit fields parsed. Purity writes a command record
([user (local)]@array(id) purevol destroy v1. Message ID: N ...) for
every CLI, GUI and REST command and a Key: 'value' record for sessions;
both become user, action, command, result and the rest. An array
forwards its controllers' whole system log to a syslog target, so the
listener keeps the purity.alert, purity.audit and purity.test
programs and drops everything else (programs; all passes it through as
kind other). The array's own service accounts count as automation
(system_users). allowed_sources restricts the senders it accepts (empty =
any). arrays maps an array's own name or its syslog hostname to the
inventory name: the message's "Array name" field is tried first, then the
syslog hostname, which is the controller's.
The zero-code path needs neither plugin: Prometheus scrapes the array's
native OpenMetrics exporter, prometheus/flasharray.rules.yml raises alerts
carrying a domain label, Alertmanager posts them to
ansible.eda.alertmanager, and alertmanager.yml runs the same playbooks.
The rules follow the names and labels the native exporter serves on
Purity//FA 6.10.6 (listed at the top of the rules file);
prometheus/prometheus.example.yml and alertmanager.example.yml are the
scrape and routing configurations.
Four rulesets listen on the network. Each is bound and authenticated differently (see also SECURITY.md):
-
Capacity approvals (
approval_port, default 5001): the POST must carryAuthorization: Bearer $APPROVAL_TOKEN.curl -s -X POST http://<eda host>:5001/endpoint \ -H "Authorization: Bearer $APPROVAL_TOKEN" -H 'Content-Type: application/json' \ -d '{"request_id": "<id>", "approve": true, "approver": "jane", "action": "eradicate_pending"}' -
Git push (
webhook_port, default 5000): the POST must carry GitHub'sX-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the exact body>made withGIT_WEBHOOK_SECRET; GitHub adds it when the webhook has a secret. By hand:BODY='{"ref":"refs/heads/main","after":"<sha>"}' SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$GIT_WEBHOOK_SECRET" | awk '{print $NF}') curl -s -X POST http://<eda host>:5000/endpoint \ -H "X-Hub-Signature-256: sha256=$SIG" -H 'Content-Type: application/json' -d "$BODY" -
Alertmanager (
alertmanager_host,alertmanager_port, default 5002):ansible.eda.alertmanagerhas no token option. Bind it to an interface only Alertmanager can reach and put it behind the EDA controller's event streams or a reverse proxy that authenticates. -
Syslog (
syslog_host,syslog_port, default 5140; on the array, a syslog server with the URItcp://<eda host>:5140):syslog_allowed_sourceslists the addresses or CIDR prefixes the listener accepts (empty = any). It is a filter, not authentication; UDP source addresses can be forged, which is one reasonsyslog.ymlonly notifies.syslog_programskeeps the Purity programs and drops the controllers' other system log, which the array forwards too.
Every rule that notifies runs playbooks/notify.yml. The alert runbook map
is the exception: alert_runbook.yml writes its own ticket or notify
record and posts nothing. notify.yml appends a record to the alert log
and, when SLACK_WEBHOOK_URL is set (env file; on the platform the
Slack webhook credential on the notify template), posts the same record
to a Slack incoming webhook: an icon for the severity, the severity, the
subject with the array named once, and the detail on the next line,
escaped for Slack. A failed post is reported in the job and never fails
the notification; the alert log is the record, Slack is the pager.
Only records at or above slack_min_severity are posted (an extra var,
else SLACK_MIN_SEVERITY in the environment, else warning). A warning
alert's resolved notice at info, or a request whose approval card the bot
posts anyway, is record material: it stays in the alert log and the job
output.
On the platform aap/seed.yml also creates a Slack notification template
(bot token, chat:write) that posts job failures and the controller-tier
workflow's approval, success and failure events, each with its link into
the platform (aap/README.md).
slack/approval_bot.py is the Socket Mode bot that lets the channel
decide (slack/README.md). It:
- posts every pending orchestrator and controller approval as a card with Approve and Reject buttons;
- carries the click back with the Slack user's name;
- reminds the channel at half and ninety percent of the decision window;
- rewrites the card with the outcome when the approval ends;
- pages and records an approval nobody decided, which otherwise fails its execution in silence.
decision-environment/ holds a Containerfile and an ansible-builder
definition, both on quay.io/ansible/ansible-rulebook:v1.3.2, that produce
the same tree: the collection sdodsley.flasharray_eda, built from the
checkout at image build time and installed with everpure.flasharray and
ansible.eda, py-pure-client, and the rulebooks and playbooks as plain
files under /opt/flasharray-eda for runs that name them by path. CI does
not build the image. The image is self-contained for standalone
ansible-rulebook runs, with no --source-dir; the Containerfile header
has the podman run line, which mounts vars/, inventory/ and the
baseline checkout and passes the credentials as environment variables.
ANSIBLE_COLLECTIONS_PATH in the image names
/usr/share/ansible/collections and the base image's
/app/.ansible/collections, where ansible.eda lives.
docs/PACKAGING.md has the ansible-builder facts the
definition is written around.
EDA Controller activations pass no --source-dir and resolve custom event
sources only from the collections installed in the decision environment.
The rulebooks name theirs by FQCN, so an activation on this image resolves
purefa_events and purefa_syslog from the collection it carries. On the
platform the rulebooks run as the generated variants under
extensions/eda/rulebooks/platform/ (tools/platform_rulebooks.py): the
same rules, with run_playbook replaced by run_job_template against the
seeded job templates, without the webhook rulesets (approvals and git
pushes arrive through the controller workflow and event streams there) and
without the tick-driven DR drill (the platform drills from a schedule).
aap/seed.yml creates the organization, the credential types
(FlashArray API, one credential per array; FlashArray fleet, whose
single credential carries every array; Orchestrator service account;
Slack webhook; the built-in platform type for a token), the credentials,
inventory, project, job templates, the controller-tier approval workflow
(request, human approval, reclaim), three schedules (the nightly DR drill,
the weekly notification-path self-check in which every array tests its
alert watchers, syslog servers and phone home, and the nightly check of
the orchestrator's definitions and the activations' vars against the
repository) and one rulebook activation per platform rulebook
(aap/README.md).
With orchestrator_enabled: true in the vars file, two rules hand their
events to Red Hat's automation orchestrator instead of acting locally: the
capacity-at-risk rule, whose approval gates the reclaim, and the
overdue-replication rule, whose approval gates the immediate replicated
snapshot. playbooks/orchestrator_post.yml obtains a client-credentials
token (AO_CLIENT_ID / AO_CLIENT_SECRET from the environment, forwarded
by run.sh) and posts a flasharray-eda/1 envelope to the route's EDA
trigger (orchestrator_paths); the approval and the action then run in
the orchestrator. Every attempt is recorded in the alert log and a refusal
never fails the rule. With orchestrator_fallback_local: true a capacity
handoff that produced no execution raises the local request instead.
docs/ORCHESTRATOR.md has the contract, the vars
and the mock (tests/mock_orchestrator.py) that the unit test and
tests/replay_capacity.yml run against;
orchestrator/README.md has the workflow
definitions, which tools/ao_sync.py imports.
Facts the rules are written around; docs/HARDWARE_NOTES.md has the longer list.
/auditsrejects a filter ontime./alerts/eventswants alert ids, so the alerts signal diffs/alerts. History resolutions are fixed (30 s, 5 min, 30 min, 2 h, 8 h, 1 day). Maintenance windows take 3600 to 172800 seconds. The alert watcher test needs watcher names.- Purity logs NTP changes as
purearray setattr --ntpserver, notpurentp. - ansible-rulebook: no filters or arithmetic in conditions (the plugin emits
the booleans; actions may use filters);
once_withinmust be a literal and is rejected on a multi-event condition (anomaly.ymlcarries no throttle; its 15 minutetimeoutbounds re-firing);run_playbookcopies the playbook's whole directory into a temporary project, so relative includes underplaybooks/work and files outside it do not;--varstakes one untemplated file;localhostmust be in the inventory. - State that must survive a rulebook restart lives on the array: the QoS
cap is a volume tag in namespace
eda, the approval request is a file. - Every playbook a template launches is safe to run twice with the same
extra vars: the array is left as it is, and the record says changed false
and why (already at size, inside the cooldown, already raised, already
done, reused, already expired, pending; the bridge matches an open
execution on event type, array and scope such as the protection group).
This covers a platform node retried after a gateway error.
orchestrator/README.md lists the behaviour per
template and
tests/e2e_run_twice.shruns seven of the playbooks twice on real arrays.
make check needs no array. It runs:
- black, pyflakes and yamllint over the plugins,
tools/,slack/and the tests; make lint-ao, the orchestrator definitions throughtools/ao_lint.py;- the unit tests,
python -m pytest tests/unit; - a playbook syntax check.
GitHub Actions runs the same on every push.
The unit tests cover each component without an array:
- a fake SDK client drives the alert diff and the audit cursor, and the syslog parser gets the real lines of both layouts;
- static checks validate every rulebook against ansible-rulebook's own
schema, require every lowercase variable a rulebook references to be
documented in
vars/example.yml, and require every playbook a rule runs to exist; tests/unit/test_orchestrator_bridge.pyrunsorchestrator_post.ymlagainst the mock (skipped withoutansible-playbook);test_ao_lint.pycovers every linter rule with a fixture and checks that every${node.artifacts.key}a definition reads is published by that template's playbook;test_ao_sync.pydrives the create, update, publish and dry-run paths against an in-process mock of the orchestrator API;test_approval_bot.pycovers the cards, the decision routing and both approval APIs with a fake HTTP function and a fake Slack client;test_playbook_artifacts.pycovers theset_statsartifacts every playbook a workflow node may follow publishes, the credential guard and the seed;test_schedules.pycovers the seed's schedules and the playbooks they launch.
tests/replay_*.yml are copies of the live rules driven by
ansible.eda.generic from the fixtures under tests/fixtures/, or from
an inline payload (replay_capacity.yml), because ansible-rulebook has no
include. They stay rule-for-rule identical to the real rulebooks, throttles
included; the unit test fails when they drift.
tests/e2e_sources.sh, tests/e2e_change_control.sh,
tests/e2e_replication.sh, tests/e2e_approvals.sh and
tests/e2e_run_twice.sh (seven playbooks twice) run the rulebooks
against live arrays. They induce what can be induced (NTP drift, a second
user's changes, a paused replica link, a maintenance window, a
pending-eradication volume, a busy volume capped and uncapped) and replay
fixtures for what cannot (hardware failures, a degraded stretched pod, the
anomaly signals); the replayed events still drive the real actions on the
array. The scripts share one contract:
- they source
env.sh; - they read the array credentials from the environment variables named by
A_URL_ENV/A_TOKEN_ENV(defaultARRAY_A_URL/ARRAY_A_TOKEN); - they take array and object names from environment variables with no defaults, and each script prints a usage line when one is missing;
- they derive their own vars file into
tests/out/vars_test.ymlfromFAEDA_VARSwithalert_logandrequest_dirrewritten undertests/out/; - they never delete anything outside
tests/out/.
tests/e2e_approvals.sh refuses its eradicate step when the array already
holds pending-eradication objects, unless E2E_ERADICATE_ALL=1 is set.
docs/VERIFICATION.md lists what each script
exercises on hardware and what is replayed only.
tests/syslog_send.py sends representative lines to the syslog listener.
The Alertmanager path is exercised by POSTing a webhook payload to
alertmanager.yml, for example:
curl -s -X POST http://127.0.0.1:5002/alerts -H 'Content-Type: application/json' -d '{
"status": "firing", "alerts": [{"status": "firing",
"labels": {"alertname": "FlashArrayHardwareComponentNotOk", "array_name": "array-a",
"component_name": "CT1", "component_type": "controller",
"severity": "critical", "domain": "health"},
"annotations": {"summary": "CT1 is critical"}}]}'
- Blog post: "Your FlashArray is already an event source" (https://theansibleguy.com/your-flasharray-is-already-an-event-source/), the ideas behind this project.
- Predecessor: the "Fusion MCP + Ansible thought experiment" (https://github.com/sdodsley/fusion-ansible-mcp).
- Collection: https://github.com/Everpure-Ansible/FlashArray-Collection (module bugs go there, not here)
- Rulebook docs: https://docs.ansible.com/projects/rulebook/en/latest/
Apache 2.0, see LICENSE.