Add dynamic Docker membership reconcile and weekly Joanna review

pull/1641/head
Carlo Costanzo 7 hours ago
parent 0958ad301d
commit 0463c88600

@ -199,5 +199,8 @@
type: custom:button-card
template: bearstone_infra_container_row
icon: mdi:docker
exclude:
- state: unavailable
- state: unknown
sort:
method: name

@ -614,7 +614,7 @@ bearstone_infra_container_row:
}
const switchEntity = key ? `switch.${key}_container` : '';
const switchEntityAlt = key ? `switch.${key}_container_2` : '';
const monitored = states['switch.docker_monitored_containers']?.attributes?.entity_id;
const monitored = states['sensor.docker_monitored_switch_inventory']?.attributes?.entity_id;
const restartCandidates = key ? [
`button.${key}_restart_container`,
`button.${key}_restart_container_2`,

@ -11,6 +11,7 @@
# Notes: Reboots are handled directly on each host by apt_weekly.sh.
# Notes: Reboot staggering: docker_14 first, docker_69 second, docker_10 third.
# Notes: Container monitoring is dynamic with binary_sensor status preferred over switch state.
# Notes: Weekly Joanna reconcile checks discovered container switches vs configured group members.
# Notes: Includes Portainer stack status repairs, 20-minute Joanna dispatch for persistent container outages, and scheduled image prune.
######################################################################
@ -203,18 +204,117 @@ template:
{% endif %}
- sensor:
- name: "Docker Monitored Switch Inventory"
unique_id: docker_monitored_switch_inventory
icon: mdi:docker
state: >-
{% set ns = namespace(items=[]) %}
{% for item in states.switch %}
{% set ent = item.entity_id %}
{% if ent is search('^switch\\..*_container(?:_2)?$') %}
{% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
{% set has_companion = expand('binary_sensor.' ~ key ~ '_status') | count > 0
or expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
or expand('sensor.' ~ key ~ '_state') | count > 0
or expand('sensor.' ~ key ~ '_state_2') | count > 0 %}
{% set switch_state = states(ent) | lower %}
{% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %}
{% if ent not in ns.items %}
{% set ns.items = ns.items + [ent] %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{{ ns.items | sort | count }}
attributes:
entity_id: >-
{% set ns = namespace(items=[]) %}
{% for item in states.switch %}
{% set ent = item.entity_id %}
{% if ent is search('^switch\\..*_container(?:_2)?$') %}
{% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
{% set has_companion = expand('binary_sensor.' ~ key ~ '_status') | count > 0
or expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
or expand('sensor.' ~ key ~ '_state') | count > 0
or expand('sensor.' ~ key ~ '_state_2') | count > 0 %}
{% set switch_state = states(ent) | lower %}
{% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %}
{% if ent not in ns.items %}
{% set ns.items = ns.items + [ent] %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{{ ns.items | sort }}
configured_group_members: >-
{{ state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list | sort }}
missing_from_group: >-
{% set discovered_ns = namespace(items=[]) %}
{% for item in states.switch %}
{% set ent = item.entity_id %}
{% if ent is search('^switch\\..*_container(?:_2)?$') %}
{% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
{% set has_companion = expand('binary_sensor.' ~ key ~ '_status') | count > 0
or expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
or expand('sensor.' ~ key ~ '_state') | count > 0
or expand('sensor.' ~ key ~ '_state_2') | count > 0 %}
{% set switch_state = states(ent) | lower %}
{% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %}
{% if ent not in discovered_ns.items %}
{% set discovered_ns.items = discovered_ns.items + [ent] %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% set discovered = discovered_ns.items | list %}
{% set configured = state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list %}
{% set ns = namespace(items=[]) %}
{% for ent in discovered %}
{% if ent not in configured %}
{% set ns.items = ns.items + [ent] %}
{% endif %}
{% endfor %}
{{ ns.items | sort }}
stale_group_members: >-
{% set discovered_ns = namespace(items=[]) %}
{% for item in states.switch %}
{% set ent = item.entity_id %}
{% if ent is search('^switch\\..*_container(?:_2)?$') %}
{% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
{% set has_companion = expand('binary_sensor.' ~ key ~ '_status') | count > 0
or expand('binary_sensor.' ~ key ~ '_status_2') | count > 0
or expand('sensor.' ~ key ~ '_state') | count > 0
or expand('sensor.' ~ key ~ '_state_2') | count > 0 %}
{% set switch_state = states(ent) | lower %}
{% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %}
{% if ent not in discovered_ns.items %}
{% set discovered_ns.items = discovered_ns.items + [ent] %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% set discovered = discovered_ns.items | list %}
{% set configured = state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list %}
{% set ns = namespace(items=[]) %}
{% for ent in configured %}
{% if ent not in discovered %}
{% set ns.items = ns.items + [ent] %}
{% endif %}
{% endfor %}
{{ ns.items | sort }}
- name: "Docker Monitored Container Count"
unique_id: docker_monitored_container_count
icon: mdi:format-list-numbered
state: >-
{{ state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | count }}
{{ state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | count }}
- name: "Docker Monitored Unavailable Count"
unique_id: docker_monitored_unavailable_count
icon: mdi:lan-disconnect
state: >-
{% set ns = namespace(keys=[], unavailable=0) %}
{% set monitored = state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) %}
{% set monitored = state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) %}
{% for switch_entity in monitored %}
{% set key = switch_entity | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
{% if key not in ns.keys %}
@ -258,7 +358,7 @@ template:
icon: mdi:docker
state: >-
{% set ns = namespace(keys=[], down=[]) %}
{% set monitored = state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list %}
{% set monitored = state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list %}
{% set telemetry_degraded = is_state('binary_sensor.docker_container_telemetry_degraded', 'on') %}
{% for switch_entity in monitored %}
{% set key = switch_entity | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
@ -302,7 +402,7 @@ template:
attributes:
down_containers: >-
{% set ns = namespace(keys=[], down=[]) %}
{% set monitored = state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list %}
{% set monitored = state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list %}
{% set telemetry_degraded = is_state('binary_sensor.docker_container_telemetry_degraded', 'on') %}
{% for switch_entity in monitored %}
{% set key = switch_entity | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
@ -438,7 +538,7 @@ script:
status_entity_alt: "binary_sensor.{{ container_key }}_status_2"
state_entity: "sensor.{{ container_key }}_state"
state_entity_alt: "sensor.{{ container_key }}_state_2"
monitored_switches: "{{ state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) }}"
monitored_switches: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) }}"
tracked_container: "{{ switch_entity in monitored_switches or switch_entity_alt in monitored_switches }}"
effective_entity: >-
{% if expand(status_entity) | count > 0 %}
@ -799,7 +899,7 @@ automation:
entity_id: "{{ trigger.event.data.entity_id | default('') }}"
old_state: "{{ (trigger.event.data.old_state.state if trigger.event.data.old_state is not none else '') | lower }}"
new_state: "{{ (trigger.event.data.new_state.state if trigger.event.data.new_state is not none else '') | lower }}"
monitored_switches: "{{ state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list }}"
monitored_switches: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list }}"
is_monitored_container_event: >-
{% set ent = entity_id %}
{% if ent.startswith('switch.') and (ent.endswith('_container') or ent.endswith('_container_2')) %}
@ -889,7 +989,7 @@ automation:
minutes: "/55"
action:
- variables:
monitored_switches: "{{ state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list }}"
monitored_switches: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list }}"
- repeat:
for_each: "{{ monitored_switches }}"
sequence:
@ -1010,11 +1110,64 @@ automation:
- service: homeassistant.update_entity
target:
entity_id:
- sensor.docker_monitored_switch_inventory
- sensor.docker_monitored_unavailable_count
- sensor.docker_containers_down_list
- sensor.docker_containers_down_count
- binary_sensor.docker_container_telemetry_degraded
- alias: "Docker Group Reconcile - Weekly Joanna Review"
id: docker_group_reconcile_weekly_joanna_review
description: "Weekly reconciliation of discovered Docker container entities vs configured group members."
mode: single
trigger:
- platform: time
at: "08:45:00"
condition:
- condition: time
weekday:
- sun
action:
- variables:
discovered_members: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list }}"
configured_members: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'configured_group_members') | default([], true) | list }}"
missing_members: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'missing_from_group') | default([], true) | list }}"
stale_members: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'stale_group_members') | default([], true) | list }}"
- service: script.send_to_logbook
data:
topic: "DOCKER"
message: >-
Weekly reconcile check: discovered={{ discovered_members | count }},
configured={{ configured_members | count }},
missing={{ missing_members | count }},
stale={{ stale_members | count }}.
- choose:
- conditions:
- condition: template
value_template: "{{ (missing_members | count > 0) or (stale_members | count > 0) }}"
sequence:
- service: script.joanna_dispatch
data:
trigger_context: >-
HA automation docker_group_reconcile_weekly_joanna_review
(Docker Group Reconcile - Weekly Joanna Review)
source: "home_assistant_automation.docker_group_reconcile_weekly_joanna_review"
summary: >-
Docker group membership drift detected (missing={{ missing_members | count }},
stale={{ stale_members | count }}).
entity_ids:
- sensor.docker_monitored_switch_inventory
- switch.docker_monitored_containers
diagnostics: >-
discovered={{ discovered_members | join(', ') if (discovered_members | count > 0) else 'none' }};
configured={{ configured_members | join(', ') if (configured_members | count > 0) else 'none' }};
missing={{ missing_members | join(', ') if (missing_members | count > 0) else 'none' }};
stale={{ stale_members | join(', ') if (stale_members | count > 0) else 'none' }}.
request: >-
Reconcile Docker monitored group members in
config/packages/docker_infrastructure.yaml so configured monitoring
matches currently discovered container entities.
- alias: "Docker Weekly Prune Unused Images"
id: docker_weekly_prune_unused_images
description: "Run weekly unguarded prune actions across Docker hosts."

Loading…
Cancel
Save

Powered by TurnKey Linux.