This commit is contained in:
2026-08-11 10:34:28 +03:00
parent 59e836575b
commit 71116426e0
2 changed files with 423 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
---
- name: Remove connstats (single device, linear)
hosts: all
gather_facts: no
vars:
ssh_user: "{{ ansible_user | default('root') }}"
ssh_pass: "{{ ansible_password | default(ansible_ssh_pass) }}"
# RabbitMQ (use controls exchange + queue_controls like the reference)
rmq_host: "{{ lookup('env','RMQ_HOST') | default('10.210.12.2', true) }}"
rmq_port: "{{ lookup('env','RMQ_PORT') | default('15672', true) }}"
rmq_user: "{{ lookup('env','RMQ_USER') | default('admin', true) }}"
rmq_pass: "{{ lookup('env','RMQ_PASS') | default('change_me', true) }}"
rmq_vhost: "{{ lookup('env','RMQ_VHOST') | default('app', true) }}"
rmq_exchange: "{{ lookup('env','RMQ_EXCHANGE') | default('controls', true) }}"
control_queue: "{{ lookup('env','CONTROL_QUEUE') | default('queue_controls', true) }}"
tasks:
- block:
# --- SSH reachability check ---
- name: Check SSH connectivity (raw ping)
raw: "echo ping"
register: ping_result
ignore_errors: true
- block:
############ step 2
- name: Check if connstats cron line is present
raw: |
if [ -f /etc/crontabs/root ]; then
grep -Eq '/root/connstats\.sh([[:space:]]|$)' /etc/crontabs/root
else
exit 1
fi
register: cron_grep
failed_when: false
changed_when: false
- name: Remove connstats cron line (preserve any other lines)
when: cron_grep.rc == 0
raw: |
sed -i '\|/root/connstats\.sh|d' /etc/crontabs/root
register: cron_remove
changed_when: true
- name: Check if /root/connstats.log exists
raw: "test -e /root/connstats.log"
register: connstats_log_grep
failed_when: false
changed_when: false
- name: Remove /root/connstats.log
when: connstats_log_grep.rc == 0
raw: "rm -f /root/connstats.log"
register: connstats_log_remove
changed_when: true
- name: Check if /root/connstats.sh exists
raw: "test -e /root/connstats.sh"
register: connstats_script_grep
failed_when: false
changed_when: false
- name: Remove /root/connstats.sh
when: connstats_script_grep.rc == 0
raw: "rm -f /root/connstats.sh"
register: connstats_script_remove
changed_when: true
- name: Set result status (success removed or no change)
set_fact:
result_status: >-
{{ 'SUCCESS_REMOVED'
if ((cron_grep.rc == 0)
or (connstats_log_grep.rc == 0)
or (connstats_script_grep.rc == 0))
else 'SUCCESS_NO_CHANGE' }}
when: ping_result is succeeded
- name: Set status fact (no ssh)
when: ping_result is failed
set_fact:
result_status: "NO_SSH"
rescue:
- name: Mark result as failed
set_fact:
result_status: "FAILED during {{ ansible_failed_task.name }}"
always:
- name: Compute inscope device
set_fact:
inscope_device_name: "{{ ansible_hostname | default(inventory_hostname) }}"
# custom field update (per your sample)
- name: Publish custom-field update connstats removed to control queue
when: result_status == 'SUCCESS_REMOVED' or result_status == 'SUCCESS_NO_CHANGE'
delegate_to: localhost
uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers: { content-type: "application/json" }
body_format: json
body:
properties: { content_type: "application/json" }
routing_key: "{{ control_queue }}"
payload: "{{ {
'inscope_device': inscope_device_name,
'task_name': 'custom_field_set',
'task_add1': 'connstats',
'task_result': 'removed'
} | to_json }}"
payload_encoding: "string"
register: rmq_cf
changed_when: false
# final wrap-up journal "connstats: ..." with actions performed
- name: Build actions list
set_fact:
_actions_list: >-
{{
[]
+ (((cron_grep is defined) and ((cron_grep.rc | default(1)) == 0)) | ternary(['removed connstats crontab entry'], []))
+ (((connstats_log_grep is defined) and ((connstats_log_grep.rc | default(1)) == 0)) | ternary(['removed connstats.log'], []))
+ (((connstats_script_grep is defined) and ((connstats_script_grep.rc | default(1)) == 0)) | ternary(['removed connstats.sh'], []))
}}
- name: Build actions string
set_fact:
_actions_str: "{{ ((_actions_list | default([])) | length > 0) | ternary((_actions_list | join(', ')), 'no changes needed') }}"
- name: Build wrap-up journal payload
delegate_to: localhost
set_fact:
wrap_payload:
inscope_device: "{{ inscope_device_name }}"
task_name: "journal_add"
task_result: >-
connstats: {{ 'success' if (result_status == 'SUCCESS_REMOVED' or result_status == 'SUCCESS_NO_CHANGE') else result_status | lower }}
— actions: {{ _actions_str }}
- name: Publish wrap-up journal to control queue
delegate_to: localhost
uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers:
content-type: "application/json"
body_format: json
body:
properties:
content_type: "application/json"
routing_key: "{{ control_queue }}"
payload: "{{ wrap_payload | to_json }}"
payload_encoding: "string"
register: rmq_wrap
changed_when: (rmq_wrap.json is defined) and (rmq_wrap.json.routed | default(false) | bool)
# Local summary (kept for operator visibility)
- name: Summary
debug:
msg:
- "result_status: {{ result_status }}"
- "we're good"

View File

@@ -0,0 +1,244 @@
# connstats-scheduler-remover.yml
# Single nbplay invocation:
# Phase 1: Cloud -> NetBox sync (via sot-updater-iponly.yml in wrapper mode)
# Phase 2: Refresh ansible_host from nb_ip, reset_connection
# Phase 3: Run connstats-remove.yml
- hosts: all
gather_facts: no
vars:
rmq_host: "{{ lookup('env','RMQ_HOST') | default('10.210.12.2', true) }}"
rmq_port: "{{ lookup('env','RMQ_PORT') | default('15672', true) }}"
rmq_user: "{{ lookup('env','RMQ_USER') | default('admin', true) }}"
rmq_pass: "{{ lookup('env','RMQ_PASS') | default('change_me', true) }}"
rmq_vhost: "{{ lookup('env','RMQ_VHOST') | default('app', true) }}"
rmq_exchange: "{{ lookup('env','RMQ_EXCHANGE') | default('controls', true) }}"
control_queue: "{{ lookup('env','CONTROLQUEUE') | default('queue_controls', true) }}"
tasks: []
- import_playbook: sot-updater-iponly.yml
vars:
sot_wrapper_mode: true
- name: Phase 2 | Refresh in-memory target IP after iponly
hosts: all
gather_facts: no
vars:
rmq_host: "{{ lookup('env','RMQ_HOST') | default('10.210.12.2', true) }}"
rmq_port: "{{ lookup('env','RMQ_PORT') | default('15672', true) }}"
rmq_user: "{{ lookup('env','RMQ_USER') | default('admin', true) }}"
rmq_pass: "{{ lookup('env','RMQ_PASS') | default('change_me', true) }}"
rmq_vhost: "{{ lookup('env','RMQ_VHOST') | default('app', true) }}"
rmq_exchange: "{{ lookup('env','RMQ_EXCHANGE') | default('controls', true) }}"
control_queue: "{{ lookup('env','CONTROLQUEUE') | default('queue_controls', true) }}"
tasks:
- name: "Wrapper | Extract nb_ip from nb_onedevice_update.py stdout"
vars:
_lines: "{{ (nb_preflight.stdout_lines | default([])) | map('regex_replace','\r','') | map('trim') | list }}"
_ok_line: "{{ (_lines | select('match','^OK\\s+') | list | last | default('')) }}"
_fail_line: "{{ (_lines | select('match','^FAIL\\s+') | list | last | default('')) }}"
_ip_raw: "{{ _ok_line | regex_search('([0-9]{1,3}(?:\\.[0-9]{1,3}){3})') | default('') }}"
set_fact:
nb_ip: "{{ _ip_raw }}"
nb_ip_ok: "{{ (_ip_raw | length) > 0 }}"
nb_ip_err: "{{ _fail_line }}"
changed_when: false
- name: "Wrapper | Abort if nb_ip not available"
fail:
msg: "Wrapper preflight did not produce nb_ip. FAIL line: {{ nb_ip_err | default('') }}"
when: not nb_ip_ok
- name: "Wrapper | Debug before ansible_host override"
debug:
msg:
- "inventory_hostname={{ inventory_hostname }}"
- "ansible_host(before)={{ ansible_host | default('UNSET') }}"
- "ansible_port={{ ansible_port | default('22') }}"
- "nb_ip={{ nb_ip | default('UNSET') }}"
changed_when: false
- name: "Wrapper | Update ansible_host to nb_ip"
set_fact:
ansible_host: "{{ nb_ip }}"
changed_when: false
- name: "Wrapper | Debug after ansible_host override"
debug:
msg:
- "inventory_hostname={{ inventory_hostname }}"
- "ansible_host(after)={{ ansible_host | default('UNSET') }}"
- "ansible_port={{ ansible_port | default('22') }}"
changed_when: false
- name: "Wrapper | Reset connection after ansible_host change"
meta: reset_connection
- name: "Wrapper | Debug after reset_connection"
debug:
msg:
- "inventory_hostname={{ inventory_hostname }}"
- "ansible_host(post_reset)={{ ansible_host | default('UNSET') }}"
- "ansible_port={{ ansible_port | default('22') }}"
changed_when: false
- name: "Wrapper | Pause 2s before next playbook"
pause:
seconds: 2
changed_when: false
- import_playbook: connstats-remove.yml
vars:
is_run_by: "scheduler"
- name: Phase 4 | NetBox wrapup and chaining for connstats removal
hosts: all
gather_facts: no
vars:
rmq_host: "{{ lookup('env','RMQ_HOST') | default('10.210.12.2', true) }}"
rmq_port: "{{ lookup('env','RMQ_PORT') | default('15672', true) }}"
rmq_user: "{{ lookup('env','RMQ_USER') | default('admin', true) }}"
rmq_pass: "{{ lookup('env','RMQ_PASS') | default('change_me', true) }}"
rmq_vhost: "{{ lookup('env','RMQ_VHOST') | default('app', true) }}"
rmq_exchange: "{{ lookup('env','RMQ_EXCHANGE') | default('controls', true) }}"
control_queue: "{{ lookup('env','CONTROLQUEUE') | default('queue_controls', true) }}"
tasks:
- name: Wrapper compute connstats removal outcome
set_fact:
inscope_device_name: "{{ ansible_hostname | default(inventory_hostname) }}"
connstats_ok: "{{ (result_status | default('') | regex_search('^SUCCESS_')) is not none }}"
connstats_status: "{{ result_status | default('UNKNOWN') }}"
changed_when: false
- name: Wrapper pause before clear action_next_timestamp
ansible.builtin.pause:
seconds: 2
delegate_to: localhost
changed_when: false
- name: Wrapper clear action_next_timestamp
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers:
content-type: "application/json"
body_format: json
body:
properties:
content_type: "application/json"
routing_key: "{{ control_queue }}"
payload: "{{ { 'inscope_device': inscope_device_name, 'task_name': 'custom_field_set', 'task_add1': 'action_next_timestamp', 'task_result': '' } | to_json }}"
payload_encoding: "string"
changed_when: false
- name: Wrapper pause before set action_state
ansible.builtin.pause:
seconds: 2
delegate_to: localhost
changed_when: false
- name: Wrapper set action_state
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers:
content-type: "application/json"
body_format: json
body:
properties:
content_type: "application/json"
routing_key: "{{ control_queue }}"
payload: "{{ { 'inscope_device': inscope_device_name, 'task_name': 'custom_field_set', 'task_add1': 'action_state', 'task_result': (connstats_ok | ternary('done','failed')) } | to_json }}"
payload_encoding: "string"
changed_when: false
- name: Wrapper pause before chaining to sot updater
ansible.builtin.pause:
seconds: 2
delegate_to: localhost
changed_when: false
- name: Wrapper set action_next to sot-updater-scheduler
when: connstats_ok
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers:
content-type: "application/json"
body_format: json
body:
properties:
content_type: "application/json"
routing_key: "{{ control_queue }}"
payload: "{{ { 'inscope_device': inscope_device_name, 'task_name': 'custom_field_set', 'task_add1': 'action_next', 'task_result': 'sot-updater-scheduler' } | to_json }}"
payload_encoding: "string"
changed_when: false
- name: Wrapper publish sot-updater-scheduler work message
when: connstats_ok
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ 'deviceconfig' | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers:
content-type: "application/json"
body_format: json
body:
properties:
content_type: "application/json"
routing_key: "deviceconfig"
payload: "{{ { 'inscope_device': inscope_device_name, 'task_name': 'sot-updater-scheduler' } | to_json }}"
payload_encoding: "string"
changed_when: false
- name: Wrapper pause before final journal
ansible.builtin.pause:
seconds: 2
delegate_to: localhost
changed_when: false
- name: Wrapper final journal report
delegate_to: localhost
ansible.builtin.uri:
url: "http://{{ rmq_host }}:{{ rmq_port }}/api/exchanges/{{ rmq_vhost | urlencode }}/{{ rmq_exchange | urlencode }}/publish"
method: POST
user: "{{ rmq_user }}"
password: "{{ rmq_pass }}"
force_basic_auth: true
status_code: 200
headers:
content-type: "application/json"
body_format: json
body:
properties:
content_type: "application/json"
routing_key: "{{ control_queue }}"
payload: "{{ { 'inscope_device': inscope_device_name, 'task_name': 'journal_add', 'task_result': (connstats_ok | ternary('connstats: successfully removed','connstats: removal failed')) ~ ' (' ~ connstats_status ~ ')' } | to_json }}"
payload_encoding: "string"
changed_when: false