와, 이게 되네요~~~

HomeLab 을 구성하여 사용 중 인데요.

처음엔 시작할 때 게스트가 몇개 되지 않아 신경쓰지 않았는데요.
Reverse Proxy를 구축하고 나서 부터 게스트가 늘어나기 시작했답니다.
그래서 phpipam 이 유트브에서 보여 관리를 위해 설치하여 스캔하였는데요.

근데, 이게 ARP, ICMP 만 가지고 스캔을 하여 다른 네트워크의 MAC 주소라든가, Hostname 이 보이지 않아요.
이렇게 사용하다, phpipam에 API를 통해 입력이 가능하다는 것을 알게 되었죠.

해서 pfsense 를 통해 arp 를 snmp로 가지고 와서 mac 주소를 업데이트 하면 어떨까 하여 Gemini와 함께 작업하니 스크립트가 만들어 졌답니다.

Hostname 은 어제 연동했는데요.
처음엔, nmap 을 연동하는 유트브가 있어 해볼려고 했는데, 호스트 이름이 보이는게 있구, 않보이는게 있더라구요. 해서 이건 힘들겠다 싶었죠.

해서 LLDP를 사용하기로 결정하고 ansible 을 통해 모든 게스트와 호스트에 lldp 를 올렸어요.
LLDP는 링크간 연결에서 네이버 정보를 가지고 오는 것이라 그런지,
호스트에서 lldp 를 확인하니 설치된 게스트가 죄다 보이더라구요.

해서 2대의 호스트에서 lldp 정보를 가지고 와 csv 로 떨구는 playbook을 만들었답니다.

그리고, 마지막으로 csv을 phpipam에 올리는 스크립트를 만들어 돌리니 호스트 이름이 업데이트 되었답니다.

작년만 해도 꿈도 꾸지 못했던 일들이 Gemini와 함께 작업하니 마무리가 되네요.

1개의 좋아요
  • app 토큰 생성하기
  • MAC 주소 phpipam 에 업데이트
    • #!/bin/bash
      
      # --- 설정 변수 ---
      PFSENSE_IP="192.168.55.254"
      COMMUNITY="SNMP_커뮤니티정보"
      # ipNetToMediaPhysAddress OID
      ARP_OID=".1.3.6.1.2.1.4.22.1.2"
      
      # --- phpIPAM 설정 (환경에 맞게 변경하세요) ---
      # API 서버 URL (예: https://phpipam.example.com)
      API_BASE="https://ipam.gnsinfo.mooo.com"
      # phpIPAM에서 발급한 토큰 (권장: 환경변수로 설정)
      API_TOKEN="jHQq-토큰정보Syo"
      # phpIPAM의 app ID (관리자 페이지에서 확인)
      PHPIPAM_APP_ID="arp_sync"
      # 기본 서브넷 ID (주소를 생성할 때 사용; 필요시 --subnet-id 로 오버라이드)
      PHPIPAM_SUBNET_ID=""
      
      # 동작 옵션
      API_PUSH=false
      DRY_RUN=false
      
      # 인자 파싱
      while [ "$#" -gt 0 ]; do
          case "$1" in
              -p|--push)
                  API_PUSH=true; shift ;;
              -d|--dry-run)
                  DRY_RUN=true; shift ;;
              --api-base)
                  API_BASE="$2"; shift 2 ;;
              --token)
                  API_TOKEN="$2"; shift 2 ;;
              --phpipam-app)
                  PHPIPAM_APP_ID="$2"; shift 2 ;;
              --subnet-id)
                  PHPIPAM_SUBNET_ID="$2"; shift 2 ;;
              *) shift ;;
          esac
      done
      
      # phpIPAM: IP이 존재하는지 확인하고 id를 반환 (없으면 빈값)
      phpipam_find_address_id() {
          local ip="$1" url resp id
          url="${API_BASE%/}/api/$PHPIPAM_APP_ID/addresses/search/$ip"
          if [ "$DRY_RUN" = true ]; then
              echo "DRY-RUN: GET $url" >&2
              return 0
          fi
      
          resp=$(curl -sS -H "token: $API_TOKEN" "$url")
          # JSON에서 "id":NUMBER 를 찾아 첫 숫자 반환
          id=$(echo "$resp" | grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -n1 | grep -oE '[0-9]+')
          echo "$id"
      }
      
      # phpIPAM: 주어진 IP를 포함하는 서브넷 ID를 찾음
      phpipam_find_subnet_id_for_ip() {
          local ip="$1" url resp tmpfile id subnet mask
          url="${API_BASE%/}/api/$PHPIPAM_APP_ID/subnets/"
          if [ "$DRY_RUN" = true ]; then
              echo "DRY-RUN: GET $url" >&2
              return 0
          fi
      
          # 전체 서브넷 목록을 가져와서 임시 파일에 저장
          resp=$(curl -sS -H "token: $API_TOKEN" "$url")
          tmpfile=$(mktemp)
          echo "$resp" > "$tmpfile"
      
          # 파싱: 각 항목에서 id, subnet, mask 추출 -> python으로 포함 여부 검사
          # 형식: id subnet/mask
          python3 - <<PY > "$tmpfile.out" 2>/dev/null
      import sys, json, ipaddress
      data=json.load(open('$tmpfile'))
      for item in data.get('data', []):
          id=item.get('id')
          subnet=item.get('subnet')
          mask=item.get('mask')
          if id and subnet and mask is not None:
              try:
                  net=f"{subnet}/{mask}"
                  if ipaddress.ip_address('$ip') in ipaddress.ip_network(net):
                      print(id)
                      sys.exit(0)
              except Exception:
                  continue
      sys.exit(0)
      PY
      
          id=$(cat "$tmpfile.out" 2>/dev/null | head -n1 | tr -d '\r\n')
          rm -f "$tmpfile" "$tmpfile.out"
          echo "$id"
      }
      
      # phpIPAM: 주소 생성
      phpipam_create_address() {
          local ip="$1" mac="$2" url body http_code
          url="${API_BASE%/}/api/$PHPIPAM_APP_ID/addresses/"
          # 서브넷 ID가 지정되지 않았으면 자동으로 찾아본다
          if [ -z "$PHPIPAM_SUBNET_ID" ]; then
              PHPIPAM_SUBNET_ID=$(phpipam_find_subnet_id_for_ip "$ip")
              if [ -z "$PHPIPAM_SUBNET_ID" ]; then
                  echo "ERROR: 해당 IP를 포함하는 서브넷을 phpIPAM에서 찾지 못했습니다: $ip" >&2
                  return 1
              fi
          fi
          body=$(printf '{"ip":"%s","subnetId":%s,"description":"pfSense ARP","mac":"%s"}' "$ip" "$PHPIPAM_SUBNET_ID" "$mac")
      
          if [ "$DRY_RUN" = true ]; then
              echo "DRY-RUN: curl -sS -X POST '$url' -H 'token: <TOKEN>' -H 'Content-Type: application/json' -d '$body'" >&2
              return 0
          fi
      
          http_code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST "$url" \
              -H "token: $API_TOKEN" \
              -H 'Content-Type: application/json' \
              -d "$body")
      
          echo "$http_code"
      }
      
      # phpIPAM: 주소 업데이트 (PATCH) - id 필요
      phpipam_update_address() {
          local id="$1" ip="$2" mac="$3" url body http_code
          url="${API_BASE%/}/api/$PHPIPAM_APP_ID/addresses/$id/"
          body=$(printf '{"mac":"%s","description":"pfSense ARP (updated)"}' "$mac")
      
          if [ "$DRY_RUN" = true ]; then
              echo "DRY-RUN: curl -sS -X PATCH '$url' -H 'token: <TOKEN>' -H 'Content-Type: application/json' -d '$body'" >&2
              return 0
          fi
      
          http_code=$(curl -sS -o /dev/null -w '%{http_code}' -X PATCH "$url" \
              -H "token: $API_TOKEN" \
              -H 'Content-Type: application/json' \
              -d "$body")
      
          echo "$http_code"
      }
      
      # 출력: IP와 MAC만 한 줄에 공백으로 구분하여 출력합니다.
      
      # snmpwalk 실행 및 파싱
      # 1. snmpwalk 결과 수집
      # 2. sed/awk를 이용해 OID 끝자리(IP)와 결과값(MAC)만 추출
      snmpwalk -v 2c -c "$COMMUNITY" "$PFSENSE_IP" "$ARP_OID" | \
      while IFS= read -r line; do
          # snmpwalk 출력이 여러 줄로 나뉠 수 있으므로 버퍼에 누적
          buf+="$line"$'\n'
      
          # '=' 문자가 있는 완성된 레코드가 나오면 처리
          if [[ "$line" == *"="* ]]; then
              # OID 끝의 IPv4 추출 (예: ...1.192.168.55.1 = ...)
              ip=$(echo "$buf" | sed -n 's/.*\.\([0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+\) *=.*/\1/p')
      
              # MAC 바이트 시퀀스(예: "00 0C 29 4F 35 A2" 또는 "70:5d:cc:92:0f:60") 추출
              mac_hex=$(echo "$buf" | grep -oE '([0-9A-Fa-f]{2}([ :]|$)){6,}' | tr -d ' :\n')
      
              if [ -n "$mac_hex" ]; then
                  formatted_mac=$(echo "$mac_hex" | sed 's/\(..\)/\1:/g; s/:$//' | tr '[:upper:]' '[:lower:]')
              else
                  formatted_mac=""
              fi
      
              # IP와 MAC 둘 다 있을 때만 출력
              if [ -n "$ip" ] && [ -n "$formatted_mac" ]; then
                  printf "%s %s\n" "$ip" "$formatted_mac"
      
                  # API_PUSH 옵션이 켜져 있으면 phpIPAM으로 전송 (존재하면 PATCH, 없으면 POST)
                  if [ "$API_PUSH" = true ]; then
                      if [ -z "$PHPIPAM_APP_ID" ]; then
                          echo "ERROR: PHPIPAM_APP_ID가 설정되어 있지 않습니다." >&2
                      else
                          addr_id=$(phpipam_find_address_id "$ip")
                          if [ -n "$addr_id" ]; then
                              http_code=$(phpipam_update_address "$addr_id" "$ip" "$formatted_mac")
                              if [ -n "$http_code" ]; then
                                  if [ "$http_code" = "200" ] || [ "$http_code" = "201" ] || [ "$http_code" = "204" ]; then
                                      echo "UPDATE OK: $ip $formatted_mac -> $http_code"
                                  else
                                      echo "UPDATE FAIL: $ip $formatted_mac -> HTTP $http_code"
                                  fi
                              fi
                          else
                              http_code=$(phpipam_create_address "$ip" "$formatted_mac")
                              if [ -n "$http_code" ]; then
                                  if [ "$http_code" = "200" ] || [ "$http_code" = "201" ] || [ "$http_code" = "204" ]; then
                                      echo "CREATE OK: $ip $formatted_mac -> $http_code"
                                  else
                                      echo "CREATE FAIL: $ip $formatted_mac -> HTTP $http_code"
                                  fi
                              fi
                          fi
                      fi
                  fi
              fi
      
              # 다음 레코드를 위해 버퍼 초기화
              buf=""
          fi
      done
      
  • phpipam+lldp 연동 하기
    • Ansible lldp 올리기
      • ---
        - name: Install and Enable LLDP (lldpd) on Debian/Ubuntu Systems
          hosts: all
          become: yes
          tasks:
            - name: Install lldpd package
              ansible.builtin.apt:
                name: lldpd
                state: present
                update_cache: yes
        
            - name: Ensure lldpd service is running and enabled
              ansible.builtin.systemd:
                name: lldpd
                state: started
                enabled: yes
        
    • ansible 정보 수집
      • ---
        - name: 우분투 서버 LLDP 정보 수집
          hosts: kvm_servers
          gather_facts: no 
          become: yes
          tasks:
            - name: lldpcli를 실행하여 JSON 결과 획득 
              ansible.builtin.shell: "lldpcli show neighbors -f json"
              register: lldp_output
              changed_when: false
        
            - name: JSON 데이터를 변환하여 변수에 할당 
              set_fact:
                lldp_data: "{{ lldp_output.stdout | from_json }}"
        
            - name: 인접 장비 정보 상세 출력 
              debug:
                msg: 
                  - "------------------------------------------------"
                  - "로컬 인터페이스: {{ item.key }}"
                  - "상대 장비 이름: {{ item.value.chassis.keys() | list | first }}"
                  - "상대 관리 IP: {{ item.value.chassis.values() | list | first | json_query('\"mgmt-ip\"') }}"
                  - "상대 포트 설명: {{ item.value.port.descr | default('N/A') }}"
              loop: "{{ lldp_data.lldp.interface | map('dict2items') | flatten }}"
              when: lldp_data.lldp.interface is defined
              
            - name: LLDP 리포트 파일 생성 (헤더)
              delegate_to: localhost
              run_once: true
              ansible.builtin.copy:
                dest: "./lldp_report.csv"
                content: "Source_Host,Local_Interface,Neighbor_Name,Neighbor_IP\n"
        
            - name: 수집된 정보를 CSV에 기록 
              delegate_to: localhost
              ansible.builtin.lineinfile:
                path: "./lldp_report.csv"
                line: "{{ inventory_hostname }},{{ item.key }},{{ item.value.chassis.keys() | list | first }},{{ item.value.chassis.values() | list | first | json_query('\"mgmt-ip\"') | first | default('N/A') }}"
              loop: "{{ lldp_data.lldp.interface | map('dict2items') | flatten }}"
        ---
        
  • lldp_report.csv
    • ╰─$ cat lldp_report.csv                                                                   130 ↵
      Source_Host,Local_Interface,Neighbor_Name,Neighbor_IP
      192.168.55.9,enp3s0f1,hooni,192.168.55.204
      192.168.55.204,vnet1,pfSense.home.arpa,1
      192.168.55.9,enp5s0f0,hooni-amd,192.168.55.51
      192.168.55.204,vnet2,pfSense.home.arpa,1
      192.168.55.204,vnet3,pfSense.home.arpa,1
      192.168.55.9,veth122fb30e,ReverseProxy,192.168.55.100
      192.168.55.204,enx000ec6573657,kvm,192.168.0.9
      192.168.55.9,tap53b8add4,RustDesk-Server-OSS,192.168.55.61
      192.168.55.204,vnet5,zabbix,192.168.55.250
      192.168.55.204,vnet7,hooni.mooo.com,192.168.55.13
      192.168.55.9,tapbc4e69a7,onlyoffice,192.168.55.123
      192.168.55.9,veth1bef2dad,wordpress,192.168.55.10
      192.168.55.9,veth5176ff78,opencloud,192.168.55.12
      192.168.55.9,vnet1,pfSense.home.arpa,1
      192.168.55.9,vnet2,gns3vm,192.168.0.44
      192.168.55.9,vnet3,gns3vm,192.168.0.44
      192.168.55.9,vnet4,turnserver,192.168.0.36
      192.168.55.9,vnet5,gnsinfo.mooo.com,192.168.55.11
      192.168.55.9,vnet6,splunk,192.168.55.22
      192.168.55.9,vnet7,homeassistant,192.168.0.134
      192.168.55.9,vnet8,homeassistant,192.168.0.134
      
  • phpipam api 업데이트
    • #!/bin/bash
      
      # --- 최종 설정 ---
      # 새로운 토큰과 확인된 API 경로를 적용했습니다.
      API_URL="https://ipam.gnsinfo.mooo.com/api/lldp_update"
      TOKEN="y9z8Cag토큰정보urIQpPT95"
      CSV_FILE="/home/hooni/ansible/lldp_report.csv"
      
      echo "=== phpIPAM LLDP Hostname 동기화 시작 ==="
      
      # CSV 파일 존재 여부 확인
      if [ ! -f "$CSV_FILE" ]; then
          echo "[!] 에러: $CSV_FILE 파일을 찾을 수 없습니다."
          exit 1
      fi
      
      # CSV 읽기 (헤더 제외)
      tail -n +2 "$CSV_FILE" | while IFS=, read -r src_host local_iface neighbor_name neighbor_ip
      do
          # 개행 문자 및 불필요한 공백 제거
          ip=$(echo "$neighbor_ip" | tr -d '\r' | xargs)
          name=$(echo "$neighbor_name" | tr -d '\r' | xargs)
      
          # 유효하지 않은 IP 스킵 (1 등)
          if [[ -z "$ip" || "$ip" == "1" ]]; then continue; fi
      
          # 1. IP 주소로 해당 장비의 ID 조회
          SEARCH=$(curl -s -X GET "$API_URL/addresses/search/$ip/" -H "token: $TOKEN")
          
          SUCCESS=$(echo "$SEARCH" | jq -r '.success // false' 2>/dev/null)
      
          if [[ "$SUCCESS" == "true" ]]; then
              # 검색된 결과 중 첫 번째 항목의 ID 추출
              ADDR_ID=$(echo "$SEARCH" | jq -r '.data[0].id')
              
              # 2. Hostname 및 Description 업데이트 수행 (PATCH)
              UPDATE=$(curl -s -X PATCH "$API_URL/addresses/$ADDR_ID/" \
                  -H "token: $TOKEN" \
                  -H "Content-Type: application/json" \
                  -d "{\"hostname\": \"$name\", \"description\": \"LLDP Found: $src_host($local_iface)\"}")
                  
              if [[ $(echo "$UPDATE" | jq -r '.success') == "true" ]]; then
                  echo "[OK] $ip -> $name (ID: $ADDR_ID)"
              else
                  MSG=$(echo "$UPDATE" | jq -r '.message')
                  echo "[ERR] 업데이트 실패 ($ip): $MSG"
              fi
          else
              echo "[SKIP] phpIPAM에 등록되지 않은 IP: $ip"
          fi
      done
      
      echo "=== 모든 작업 완료 ==="
      
1개의 좋아요