From dc5aff7e987a96ab6ee1ec346ce7c38707e5d15b Mon Sep 17 00:00:00 2001 From: pavel Date: Sun, 15 Feb 2026 10:38:14 +0200 Subject: [PATCH] 1038 --- .../sot-updater-scheduler.yml | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 files/ansible-playbooks/sot-updater-scheduler.yml diff --git a/files/ansible-playbooks/sot-updater-scheduler.yml b/files/ansible-playbooks/sot-updater-scheduler.yml new file mode 100644 index 0000000..c05b69c --- /dev/null +++ b/files/ansible-playbooks/sot-updater-scheduler.yml @@ -0,0 +1,194 @@ +# sot-updater-scheduler.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 full sot-updater-current.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('(?:^|\\s)ip=([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+)(?:\\s|$)', '\\1') | default('') }}" + _ip: >- + {%- if _ip_raw is string -%} + {{ _ip_raw }} + {%- elif _ip_raw is sequence and (_ip_raw | length) > 0 -%} + {{ _ip_raw[0] }} + {%- else -%} + "" + {%- endif -%} + set_fact: + wrapper_ok_line: "{{ _ok_line }}" + wrapper_fail_line: "{{ _fail_line }}" + wrapper_nb_ip: "{{ _ip | trim }}" + + + - name: "Wrapper | Debug extracted values" + ansible.builtin.debug: + msg: + - "wrapper_ok_line='{{ wrapper_ok_line }}'" + - "wrapper_fail_line='{{ wrapper_fail_line | default('') }}'" + - "wrapper_nb_ip='{{ wrapper_nb_ip }}'" + + # ----------------------- Option A: Cloud offline / no IP -> Journal + stop cleanly ----------------------- + - name: "Wrapper | Journal: device not online in cloud (skip full scan)" + delegate_to: localhost + when: (wrapper_fail_line | default('') | length) > 0 + vars: + _journal_msg: >- + during sot-updater scan the device was not online in cloud (no IP). please try later. + details: {{ wrapper_fail_line | default('') }} + 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': (ansible_hostname | default(inventory_hostname)), + 'task_name': 'journal_add', + 'task_result': _journal_msg + } | to_json }}" + payload_encoding: "string" + changed_when: false + + - name: "Wrapper | Mark host to skip full scan (cloud-offline)" + when: (wrapper_fail_line | default('') | length) > 0 + set_fact: + wrapper_skip_full_scan: true + + + - name: "Wrapper | Stop host after cloud-offline journal (no full scan)" + when: (wrapper_fail_line | default('') | length) > 0 + meta: end_host + + # ----------------------- Normal path: we have OK ip=... -> proceed ----------------------- + - name: "Wrapper | Assert IP extracted successfully" + ansible.builtin.assert: + that: + - nb_script is defined + - nb_script.stat.exists | default(false) + - (wrapper_ok_line | length) > 0 + - (wrapper_nb_ip | length) > 0 + fail_msg: >- + Wrapper stop: could not extract IP from nb_onedevice_update.py output. + ok_line='{{ wrapper_ok_line | default('') }}' + fail_line='{{ wrapper_fail_line | default('') }}' + stdout_tail={{ (nb_preflight.stdout_lines | default([]))[-12:] }} + + - name: "Wrapper | Override ansible_host to refreshed IP ({{ wrapper_nb_ip }})" + set_fact: + ansible_host: "{{ wrapper_nb_ip }}" + + - name: "Wrapper | Reset SSH connection context (ensure new ansible_host is used)" + meta: reset_connection + +- name: Phase 3 gate | Skip full scan if cloud-offline + hosts: all + gather_facts: no + + tasks: + - name: "Wrapper | Gate: end_host if wrapper_skip_full_scan is set" + when: wrapper_skip_full_scan | default(false) + meta: end_host + + +- import_playbook: sot-updater.yml + + +- name: Phase 4 | Update action fields after sot-updater + 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: "Scheduler | Set action_state done" + 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': (ansible_hostname | default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'action_state', + 'task_result': 'done' + } | to_json }}" + payload_encoding: "string" + changed_when: false + + - name: "Scheduler | Set action_last sot-updater-scheduler" + 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': (ansible_hostname | default(inventory_hostname)), + 'task_name': 'custom_field_set', + 'task_add1': 'action_last', + 'task_result': 'sot-updater-scheduler' + } | to_json }}" + payload_encoding: "string" + changed_when: false