diff --git a/files/ansible-playbooks/connstats-remove.yml b/files/ansible-playbooks/connstats-remove.yml new file mode 100644 index 0000000..b1ecc87 --- /dev/null +++ b/files/ansible-playbooks/connstats-remove.yml @@ -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" diff --git a/files/ansible-playbooks/connstats-scheduler-remover.yml b/files/ansible-playbooks/connstats-scheduler-remover.yml new file mode 100644 index 0000000..85a7cfc --- /dev/null +++ b/files/ansible-playbooks/connstats-scheduler-remover.yml @@ -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