diff --git a/playbooks/scan_inventory.yml b/playbooks/scan_inventory.yml index 758679d..275fbea 100644 --- a/playbooks/scan_inventory.yml +++ b/playbooks/scan_inventory.yml @@ -1,129 +1,176 @@ --- -- name: Сбор инвентаря Windows (Nmap SMB + DNS) +- name: Умное сканирование сети и классификация устройств hosts: localhost connection: local - gather_facts: yes + gather_facts: no + become: yes # !!! ВАЖНО: Для определения OS (nmap -O) нужны права root vars: - inventory_file: "/tmp/windows_inventory2.ini" - - # Список подсетей + # --- НАСТРОЙКИ SEMAPHORE --- + semaphore_url: "http://192.168.0.198:9999" + semaphore_project_id: 1 + semaphore_key_id: 7 + semaphore_api_token: "9ojexqiwt1xkemig7j1bd1pe-frh7hkre4reryk2occ=" + inventory_name: "Smart Network Scan" + + # --- СЕТИ --- subnets: - "192.168.0.0/24" - - "192.168.1.0/24" - - "192.168.2.0/24" - - "192.168.3.0/24" - - "172.19.8.0/23" - - "172.19.9.0/23" - - "172.19.10.0/23" - - "172.19.24.0/23" - - "172.19.26.0/23" - - "172.19.40.0/23" - - "172.19.42.0/23" - - "172.19.56.0/23" - - "172.19.58.0/23" - - # Порты для проверки доступности (WinRM, SSH, SMB) - scan_ports: - - 5985 - - 22 - - 445 + # добавьте другие подсети + + # Порты для проверки (добавил 8291 для MikroTik, 80/443 для веб-морд, 631 для принтеров) + scan_ports: [22, 445, 5985, 80, 443, 8291, 631] tasks: - - name: Сканирование сети (поиск живых IP) + # ---------------------------------------------------------------- + # ШАГ 1: Быстрый поиск живых хостов (Ping Sweep) + # Это нужно, чтобы не сканировать пустоту тяжелым сканом + # ---------------------------------------------------------------- + - name: Быстрый поиск активных IP command: > - nmap -p {{ scan_ports | join(',') }} - -Pn -n --open --min-rate 1000 -T4 -oG - + nmap -sn -n --min-rate 1000 -T4 -oG - {{ subnets | join(' ') }} - register: nmap_result + register: ping_scan changed_when: false - - name: Извлечение IP адресов + - name: Формирование списка живых IP set_fact: - active_ips: "{{ nmap_result.stdout | regex_findall('Host: ([0-9.]+).*Ports:.*(?:' + scan_ports | join('|') + ')/open') | unique | list }}" + active_ips: "{{ ping_scan.stdout | regex_findall('Host: ([0-9.]+)') | unique | list }}" - - name: Статистика - debug: - msg: "Найдено активных IP: {{ active_ips | length }}" + - name: Проверка + fail: + msg: "Живых хостов не найдено." + when: active_ips | length == 0 - # ГЛАВНОЕ ИЗМЕНЕНИЕ: Получаем имя через SMB (NetBIOS) или DNS - - name: Определение имен хостов (SMB Discovery) + # ---------------------------------------------------------------- + # ШАГ 2: Глубокий анализ каждого IP (ОС, Версии, Имена) + # Используем -O (OS), -sV (Versions) и скрипты + # ---------------------------------------------------------------- + - name: Глубокое сканирование найденных хостов shell: | - IP="{{ item }}" - # 1. Пробуем узнать имя через SMB (самый надежный способ для Windows) - SMB_NAME=$(nmap -p 445 --script smb-os-discovery $IP -Pn -n | grep "Computer name:" | awk -F': ' '{print $2}') - - if [ ! -z "$SMB_NAME" ]; then - echo "$SMB_NAME" | tr '[:upper:]' '[:lower:]' - else - # 2. Если SMB закрыт/не ответил, пробуем DNS (как раньше) - DNS_NAME=$(nslookup -timeout=1 $IP 192.168.1.250 2>/dev/null | grep 'name =' | awk '{print $NF}' | sed 's/\.$//' | head -n 1) - if [ ! -z "$DNS_NAME" ]; then - echo "$DNS_NAME" | tr '[:upper:]' '[:lower:]' - else - echo "UNKNOWN" - fi - fi + # Запускаем Nmap с определением ОС и скриптами SMB/Banner + # -O: Определение ОС (требует sudo) + # --osscan-guess: Пытаться угадать, если не уверен + # --script: Скрипты для точного имени Windows и SSH баннеров + nmap -O -sV -p {{ scan_ports | join(',') }} --script=smb-os-discovery,banner -Pn -n -T4 {{ item }} loop: "{{ active_ips }}" - register: host_names + register: deep_scan_results changed_when: false - no_log: true # Чтобы не засорять лог - # Фильтруем и готовим списки - - name: Сортировка хостов + # ---------------------------------------------------------------- + # ШАГ 3: Анализ и классификация (Магия Ansible + Jinja2) + # ---------------------------------------------------------------- + - name: Классификация хостов set_fact: - # Ищем 'pc' в любом месте имени (было ^pc - только в начале) - pc_list: >- - {{ host_names.results - | selectattr('stdout', 'search', 'pc') - | map(attribute='item') | list - | zip(host_names.results | selectattr('stdout', 'search', 'pc') | map(attribute='stdout') | list) - | list }} - - # Остальные (сервера, принтеры, неизвестные) - other_list: >- - {{ host_names.results - | rejectattr('stdout', 'search', 'pc') - | map(attribute='item') | list - | zip(host_names.results | rejectattr('stdout', 'search', 'pc') | map(attribute='stdout') | list) - | list }} + classified_hosts: [] - - name: Запись инвентаря + - name: Разбор результатов сканирования + set_fact: + classified_hosts: "{{ classified_hosts + [ host_data ] }}" + vars: + output: "{{ item.stdout }}" + ip: "{{ item.item }}" + + # --- ЛОГИКА ОПРЕДЕЛЕНИЯ ИМЕНИ --- + # Пытаемся найти имя через SMB, если нет - DNS, если нет - IP + smb_name: "{{ output | regex_search('Computer name: ([\\w-]+)', '\\1') | first | default('') }}" + dns_name_cmd: "nslookup {{ ip }} 192.168.1.250 | grep 'name =' | awk '{print $NF}' | sed 's/\\.$//'" + # (В реальном плейбуке nslookup внутри loop медленно, тут упрощено) + final_name: >- + {% if smb_name != '' %}{{ smb_name | lower }} + {% else %}host_{{ ip | replace('.', '_') }}{% endif %} + + # --- ЛОГИКА ОПРЕДЕЛЕНИЯ ТИПА --- + host_type: >- + {% if 'Windows' in output or 'Microsoft Windows' in output or '445/tcp open' in output %}windows + {% elif 'MikroTik' in output or 'RouterOS' in output or '8291/tcp open' in output %}mikrotik + {% elif 'Linux' in output or 'Ubuntu' in output or 'Debian' in output %}linux + {% elif 'HP' in output or 'Printer' in output or '631/tcp open' in output %}printer + {% else %}other{% endif %} + + # --- СОБИРАЕМ ОБЪЕКТ --- + host_data: + ip: "{{ ip }}" + name: "{{ final_name }}" + type: "{{ host_type }}" + raw_os: "{{ output | regex_search('OS details: ([^\\n]+)', '\\1') | first | default('Unknown') }}" + loop: "{{ deep_scan_results.results }}" + no_log: true + + # ---------------------------------------------------------------- + # ШАГ 4: Генерация инвентаря по группам + # ---------------------------------------------------------------- + - name: Генерация текста инвентаря + set_fact: + inventory_content: | + # === Windows Systems === + [windows] + {% for host in classified_hosts if host.type == 'windows' %} + {{ host.name }} ansible_host={{ host.ip }} # Detected: {{ host.raw_os }} + {% endfor %} + + [windows:vars] + ansible_connection=ssh + ansible_shell_type=powershell + ansible_user=o.grechko + ansible_port=22 + + # === Linux Systems === + [linux] + {% for host in classified_hosts if host.type == 'linux' %} + {{ host.name }} ansible_host={{ host.ip }} # Detected: {{ host.raw_os }} + {% endfor %} + + [linux:vars] + ansible_connection=ssh + ansible_user=root + + # === MikroTik Routers === + [routers_mikrotik] + {% for host in classified_hosts if host.type == 'mikrotik' %} + {{ host.name }} ansible_host={{ host.ip }} + {% endfor %} + + [routers_mikrotik:vars] + ansible_connection=network_cli + ansible_network_os=routeros + ansible_user=admin + + # === Printers & Others === + [others] + {% for host in classified_hosts if host.type == 'printer' or host.type == 'other' %} + {{ host.name }} ansible_host={{ host.ip }} # Type: {{ host.type }}, OS: {{ host.raw_os }} + {% endfor %} + + # ---------------------------------------------------------------- + # ШАГ 5: Отправка в Semaphore (Ваш рабочий метод через файл) + # ---------------------------------------------------------------- + - name: Сохранение JSON copy: content: | - # === Найденные PC (по имени содержит 'pc') === - [windows_pcs] - {% for ip, name in pc_list %} - {{ name }} ansible_host={{ ip }} - {% endfor %} - - # === Остальные устройства (Servers, Unknown) === - [windows_other] - {% for ip, name in other_list %} - {% if name == "UNKNOWN" %} - unknown_{{ ip | replace('.', '_') }} ansible_host={{ ip }} - {% else %} - {{ name }} ansible_host={{ ip }} - {% endif %} - {% endfor %} - - [windows:children] - windows_pcs - windows_other - - [windows:vars] - ansible_connection=winrm - ansible_winrm_transport=ntlm - ansible_winrm_server_cert_validation=ignore - ansible_port=5985 - ansible_user=Administrator - # Пароль лучше задать в Secret Store, не здесь - dest: "{{ inventory_file }}" + { + "name": "{{ inventory_name }} {{ 1000 | random }}", + "project_id": {{ semaphore_project_id | int }}, + "type": "static", + "ssh_key_id": {{ semaphore_key_id | int }}, + "become_key_id": null, + "repository_id": null, + "inventory": {{ inventory_content | to_json }} + } + dest: /tmp/semaphore_smart_payload.json - - name: ПОКАЗАТЬ РЕЗУЛЬТАТ (Скопируйте это в новый инвентарь) - command: cat {{ inventory_file }} - register: cat_inventory - changed_when: false + - name: Отправка через CURL + command: > + curl -v -X POST "{{ semaphore_url }}/api/project/{{ semaphore_project_id }}/inventory" + -H "Authorization: Bearer {{ semaphore_api_token }}" + -H "Content-Type: application/json" + -H "Accept: application/json" + -d @/tmp/semaphore_smart_payload.json + register: curl_result + ignore_errors: yes - - name: Вывод в лог + - name: Очистка + file: path=/tmp/semaphore_smart_payload.json state=absent + + - name: Результат debug: - var: cat_inventory.stdout_lines \ No newline at end of file + msg: "Проверьте инвентарь! Создан ID, если статус 200/201" \ No newline at end of file