Compare commits
7 Commits
c18d831d89
...
39f37ee2b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 39f37ee2b8 | |||
| 2afb2e3bd2 | |||
| 6278c3691f | |||
| f7ca7a0929 | |||
| 1273368c9a | |||
| dc8dbc3cd7 | |||
| 6c020e7ee3 |
@@ -1,15 +1,74 @@
|
|||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Docker build context — exclusions
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
|
# Dependencies (réinstallés dans l'image)
|
||||||
node_modules
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
.next
|
.next
|
||||||
.git
|
out
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# Env (les secrets ne doivent jamais être copiés dans l'image)
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
npm-debug.log
|
.env.production
|
||||||
Dockerfile
|
.env.development.local
|
||||||
.dockerignore
|
.env.test.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# IDE
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker (évite la récursion). On NE PEUT PAS exclure docker/ car
|
||||||
|
# l'image a besoin de docker/entrypoint.sh ; on se contente d'exclure
|
||||||
|
# le compose et le dockerignore qui sont inutiles dans l'image.
|
||||||
|
docker/docker-compose.yml
|
||||||
|
docker/docker-compose.bridge.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Bases locales (SQLite dev)
|
||||||
|
data
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
prisma/data
|
||||||
|
prisma/*.db
|
||||||
|
prisma/*.db-journal
|
||||||
|
|
||||||
|
# Tests & coverage
|
||||||
tests
|
tests
|
||||||
coverage
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
__tests__
|
||||||
|
|
||||||
|
# Docs (sauf README)
|
||||||
*.md
|
*.md
|
||||||
!README.md
|
!README.md
|
||||||
|
|
||||||
|
# CI / Misc
|
||||||
|
.github
|
||||||
|
.husky
|
||||||
|
|||||||
52
.env.docker.example
Normal file
52
.env.docker.example
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# ---------------------------------------------------------------
|
||||||
|
# IPAM — Variables pour un déploiement Docker
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Copie ce fichier en `.env` à la racine du projet avant de lancer
|
||||||
|
# `docker compose`. Le compose lit `../.env` (env_file).
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Base de données
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Choix par défaut : SQLite — aucune dépendance externe.
|
||||||
|
# Pour basculer sur PostgreSQL :
|
||||||
|
# 1) Lance le profil postgres : `docker compose --profile postgres up -d`
|
||||||
|
# 2) Décommente la section PG ci-dessous et commente celle SQLite.
|
||||||
|
DATABASE_PROVIDER=sqlite
|
||||||
|
DATABASE_URL="file:/app/data/ipam.db"
|
||||||
|
|
||||||
|
# --- Variante PostgreSQL --------------------------------------
|
||||||
|
# DATABASE_PROVIDER=postgresql
|
||||||
|
# DATABASE_URL="postgresql://ipam:ipam@127.0.0.1:5432/ipam?schema=public"
|
||||||
|
# POSTGRES_PASSWORD=ipam
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Application
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
PORT=3000
|
||||||
|
APP_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# Seed initial au premier démarrage (catégories d'apps, etc.)
|
||||||
|
RUN_SEED=true
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Découverte réseau
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Adapte la plage à ton LAN (séparées par des virgules)
|
||||||
|
DISCOVERY_DEFAULT_CIDRS=192.168.1.0/24
|
||||||
|
|
||||||
|
DISCOVERY_DEFAULT_PORTS=22,53,80,81,443,445,3000,3306,5432,6379,7878,8080,8081,8096,8123,8443,8989,9000,9090,9091,9117,9443,32400
|
||||||
|
DISCOVERY_PING_TIMEOUT=1000
|
||||||
|
DISCOVERY_PORT_TIMEOUT=800
|
||||||
|
DISCOVERY_CONCURRENCY=32
|
||||||
|
DISCOVERY_ENABLE_PING=true
|
||||||
|
DISCOVERY_ENABLE_PORT_SCAN=true
|
||||||
|
DISCOVERY_ENABLE_ARP=true
|
||||||
|
DISCOVERY_ENABLE_MDNS=true
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Logs
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
LOG_LEVEL=info
|
||||||
@@ -1,44 +1,99 @@
|
|||||||
# syntax=docker/dockerfile:1.6
|
# syntax=docker/dockerfile:1.6
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# IPAM — Dockerfile multi-stage
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Build arg :
|
||||||
|
# DATABASE_PROVIDER = sqlite (défaut) | postgresql
|
||||||
|
# Rappel : le provider Prisma est statique dans schema.prisma, on
|
||||||
|
# le bascule via scripts/switch-db-provider.mjs avant `prisma generate`.
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
|
ARG DATABASE_PROVIDER=sqlite
|
||||||
|
|
||||||
# ---------- Stage 1 : deps ----------
|
# ---------- Stage 1 : deps ----------
|
||||||
FROM node:20-alpine AS deps
|
FROM node:20-alpine AS deps
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
RUN npm ci
|
# `npm ci` exige un package-lock.json ; sinon on retombe sur `npm install`
|
||||||
|
# qui en générera un. Cela permet un build « out of the box » même sans lock.
|
||||||
|
RUN if [ -f package-lock.json ]; then \
|
||||||
|
npm ci; \
|
||||||
|
else \
|
||||||
|
echo "package-lock.json absent -> npm install" && \
|
||||||
|
npm install --no-audit --no-fund; \
|
||||||
|
fi
|
||||||
|
|
||||||
# ---------- Stage 2 : build ----------
|
# ---------- Stage 2 : build ----------
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
ARG DATABASE_PROVIDER
|
||||||
|
ENV DATABASE_PROVIDER=${DATABASE_PROVIDER}
|
||||||
|
|
||||||
|
# Valeur factice pour que Zod ne rejette pas la config à la génération
|
||||||
|
ENV DATABASE_URL="file:./data/build-placeholder.db"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npx prisma generate
|
|
||||||
RUN npm run build
|
# Bascule le provider Prisma si nécessaire, puis génère le client et build
|
||||||
|
RUN node scripts/switch-db-provider.mjs "$DATABASE_PROVIDER" \
|
||||||
|
&& npx prisma generate \
|
||||||
|
&& npm run build
|
||||||
|
|
||||||
# ---------- Stage 3 : runner ----------
|
# ---------- Stage 3 : runner ----------
|
||||||
FROM node:20-alpine AS runner
|
FROM node:20-alpine AS runner
|
||||||
WORKDIR /app
|
ARG DATABASE_PROVIDER
|
||||||
|
ENV DATABASE_PROVIDER=${DATABASE_PROVIDER}
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
# Outils système utiles à la découverte réseau
|
WORKDIR /app
|
||||||
# - iputils : ping ICMP (privileged / cap_net_raw)
|
|
||||||
# - iproute2 : ip neigh (ARP)
|
|
||||||
# - nmap : fallback scan ports (optionnel, décommente si besoin)
|
|
||||||
RUN apk add --no-cache iputils iproute2
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
# Outils système utiles à la découverte et à l'entrypoint :
|
||||||
RUN adduser --system --uid 1001 nextjs
|
# - iputils : ping ICMP (cap_net_raw requis côté compose)
|
||||||
|
# - iproute2 : `ip neigh` pour le scanner ARP
|
||||||
|
# - netcat-openbsd : attente de PostgreSQL (entrypoint)
|
||||||
|
# - openssl, tini, dumb-init : init + signaux propres
|
||||||
|
RUN apk add --no-cache iputils iproute2 netcat-openbsd tini openssl
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
# User non-root pour la sécurité
|
||||||
|
RUN addgroup --system --gid 1001 nodejs \
|
||||||
|
&& adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Binaires app (mode standalone Next)
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
# Prisma : schema + client généré + migrations (pour `migrate deploy` au boot)
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/prisma ./node_modules/prisma
|
||||||
|
# tsx + dépendances pour pouvoir exécuter prisma/seed.ts si RUN_SEED=true
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/tsx ./node_modules/tsx
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/esbuild ./node_modules/esbuild
|
||||||
|
# Binaires symlinkés (.bin) — nécessaires à `npx prisma` et `npx tsx`
|
||||||
|
# Les symlinks pointant vers des paquets non copiés seront dangling
|
||||||
|
# mais sans impact : on n'utilise que prisma et tsx au runtime.
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.bin ./node_modules/.bin
|
||||||
|
|
||||||
|
# Entrypoint
|
||||||
|
COPY --chown=nextjs:nodejs docker/entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
# Volume SQLite (inutile en PG mais inoffensif)
|
||||||
|
RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["node", "server.js"]
|
# Healthcheck simple — retourne 200 dès que Next répond
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1
|
||||||
|
|
||||||
|
# tini pour gérer SIGTERM proprement
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--", "/app/entrypoint.sh"]
|
||||||
|
|||||||
109
docker/README.md
Normal file
109
docker/README.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# IPAM — Déploiement Docker
|
||||||
|
|
||||||
|
Tout ce qu'il faut pour lancer l'application en une commande.
|
||||||
|
|
||||||
|
## Pré-requis
|
||||||
|
|
||||||
|
- Docker Engine ≥ 24
|
||||||
|
- Docker Compose v2 (`docker compose` et non `docker-compose`)
|
||||||
|
- Linux conseillé (le mode `network_mode: host` n'est pas pleinement supporté sur Docker Desktop macOS/Windows ; voir la section *Bridge mode* plus bas)
|
||||||
|
|
||||||
|
## Fichiers fournis
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `Dockerfile` | Image multi-stage (deps → build → runner) basée sur `node:20-alpine` |
|
||||||
|
| `entrypoint.sh` | Attente PostgreSQL, migrations Prisma, seed optionnel, lancement Next |
|
||||||
|
| `docker-compose.yml` | Compose principal — profils `sqlite` et `postgres`, mode host |
|
||||||
|
| `docker-compose.bridge.yml` | Override pour passer en mode bridge (sans découverte LAN) |
|
||||||
|
|
||||||
|
## Démarrage rapide — profil SQLite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Prépare la config
|
||||||
|
cp .env.docker.example .env
|
||||||
|
# (édite .env si besoin — surtout DISCOVERY_DEFAULT_CIDRS)
|
||||||
|
|
||||||
|
# 2. Build + run
|
||||||
|
docker compose -f docker/docker-compose.yml --profile sqlite up -d --build
|
||||||
|
|
||||||
|
# 3. Logs
|
||||||
|
docker compose -f docker/docker-compose.yml logs -f ipam
|
||||||
|
```
|
||||||
|
|
||||||
|
L'UI est disponible sur <http://localhost:3000>.
|
||||||
|
|
||||||
|
## Profil PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.docker.example .env
|
||||||
|
# Dans .env, bascule sur PostgreSQL :
|
||||||
|
# DATABASE_PROVIDER=postgresql
|
||||||
|
# DATABASE_URL="postgresql://ipam:ipam@127.0.0.1:5432/ipam?schema=public"
|
||||||
|
|
||||||
|
docker compose -f docker/docker-compose.yml --profile postgres up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Le container `ipam-postgres` expose le port `5432` sur l'hôte (utile pour `prisma studio` ou un client SQL externe).
|
||||||
|
|
||||||
|
## Bridge mode (sans découverte LAN)
|
||||||
|
|
||||||
|
Utile sur macOS / Windows ou pour un test rapide de l'UI :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
-f docker/docker-compose.yml \
|
||||||
|
-f docker/docker-compose.bridge.yml \
|
||||||
|
--profile sqlite up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ En bridge, **ARP / mDNS / ping sweep ne fonctionneront pas** sur ton LAN — seule la saisie manuelle reste pleinement utilisable.
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Arrêter
|
||||||
|
docker compose -f docker/docker-compose.yml down
|
||||||
|
|
||||||
|
# Tout supprimer (y compris les volumes — perte des données !)
|
||||||
|
docker compose -f docker/docker-compose.yml down -v
|
||||||
|
|
||||||
|
# Re-seeder manuellement
|
||||||
|
docker exec -it ipam npx tsx prisma/seed.ts
|
||||||
|
|
||||||
|
# Prisma studio (depuis l'hôte, profil postgres)
|
||||||
|
DATABASE_URL="postgresql://ipam:ipam@localhost:5432/ipam?schema=public" npx prisma studio
|
||||||
|
|
||||||
|
# Shell dans le container
|
||||||
|
docker exec -it ipam sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables d'environnement clés
|
||||||
|
|
||||||
|
Toutes les variables sont documentées dans [`.env.docker.example`](../.env.docker.example).
|
||||||
|
Les plus importantes pour Docker :
|
||||||
|
|
||||||
|
- `DATABASE_PROVIDER` — `sqlite` (défaut) ou `postgresql`
|
||||||
|
- `DATABASE_URL` — fichier `file:/app/data/ipam.db` ou DSN PostgreSQL
|
||||||
|
- `RUN_SEED` — `true` pour exécuter `prisma/seed.ts` au premier boot
|
||||||
|
- `DISCOVERY_DEFAULT_CIDRS` — plages scannées par défaut, à adapter à ton LAN
|
||||||
|
|
||||||
|
## Persistance
|
||||||
|
|
||||||
|
| Volume | Contenu |
|
||||||
|
|---|---|
|
||||||
|
| `ipam-data` | Base SQLite (`/app/data/ipam.db`) |
|
||||||
|
| `ipam-postgres` | Données PostgreSQL |
|
||||||
|
|
||||||
|
Les deux survivent à `docker compose down`, mais pas à `down -v`.
|
||||||
|
|
||||||
|
## Pourquoi `network_mode: host` ?
|
||||||
|
|
||||||
|
Pour qu'un container puisse :
|
||||||
|
- envoyer des paquets ICMP (ping sweep)
|
||||||
|
- lire la table ARP (`ip neigh`) du réseau local
|
||||||
|
- recevoir les annonces mDNS (port 5353 UDP multicast)
|
||||||
|
|
||||||
|
…il doit partager la pile réseau de l'hôte. Sans `host`, ces fonctionnalités sont aveugles au LAN.
|
||||||
|
|
||||||
|
Les `cap_add: [NET_RAW, NET_ADMIN]` permettent par ailleurs au binaire `ping` du container d'ouvrir un socket ICMP en non-root.
|
||||||
47
docker/docker-compose.bridge.yml
Normal file
47
docker/docker-compose.bridge.yml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# ---------------------------------------------------------------
|
||||||
|
# IPAM — Override : bridge network (sans host mode)
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# À combiner avec docker-compose.yml :
|
||||||
|
# docker compose \
|
||||||
|
# -f docker/docker-compose.yml \
|
||||||
|
# -f docker/docker-compose.bridge.yml \
|
||||||
|
# --profile sqlite up -d
|
||||||
|
#
|
||||||
|
# ATTENTION : en mode bridge la découverte ARP / mDNS / ping sweep
|
||||||
|
# ne fonctionnera PAS sur le LAN (le container n'a pas accès direct
|
||||||
|
# au réseau de l'hôte). À utiliser uniquement pour la saisie manuelle
|
||||||
|
# ou un test rapide de l'UI sur macOS / Windows / Docker Desktop.
|
||||||
|
#
|
||||||
|
# `!reset null` retire `network_mode: host` hérité du compose parent.
|
||||||
|
# Nécessaire car `network_mode` et `networks` sont mutuellement
|
||||||
|
# exclusifs. Requiert Docker Compose v2.24+.
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
|
services:
|
||||||
|
ipam:
|
||||||
|
network_mode: !reset null
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
networks:
|
||||||
|
- ipam-net
|
||||||
|
|
||||||
|
ipam-pg:
|
||||||
|
network_mode: !reset null
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
# En bridge, on cible le service postgres par son nom DNS Docker
|
||||||
|
DATABASE_URL: "postgresql://ipam:${POSTGRES_PASSWORD:-ipam}@postgres:5432/ipam?schema=public"
|
||||||
|
networks:
|
||||||
|
- ipam-net
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
network_mode: !reset null
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- ipam-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ipam-net:
|
||||||
|
driver: bridge
|
||||||
@@ -2,60 +2,115 @@
|
|||||||
# IPAM — Docker Compose
|
# IPAM — Docker Compose
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
# Deux profils :
|
# Deux profils :
|
||||||
# - `sqlite` → ipam seul, base dans un volume
|
# - `sqlite` → ipam seul, base SQLite dans un volume
|
||||||
# - `postgres` → ipam + postgres
|
# - `postgres` → ipam + postgres
|
||||||
#
|
#
|
||||||
# Usage :
|
# Usage :
|
||||||
# docker compose --profile sqlite up -d
|
# docker compose -f docker/docker-compose.yml --profile sqlite up -d
|
||||||
# docker compose --profile postgres up -d
|
# docker compose -f docker/docker-compose.yml --profile postgres up -d
|
||||||
|
#
|
||||||
|
# Le mode `network_mode: host` est indispensable pour que la découverte
|
||||||
|
# (ARP, mDNS, ping sweep, port scan) atteigne le LAN. Sans lui, le
|
||||||
|
# container ne voit que le bridge Docker.
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
# =========================================================
|
||||||
|
# Service IPAM — variante SQLite (autonome, sans PostgreSQL)
|
||||||
|
# =========================================================
|
||||||
ipam:
|
ipam:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: docker/Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
|
args:
|
||||||
|
DATABASE_PROVIDER: sqlite
|
||||||
|
image: ipam-homelab:sqlite
|
||||||
container_name: ipam
|
container_name: ipam
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# Network mode host : indispensable pour ARP / mDNS / ping sweep
|
|
||||||
# sur le réseau local. Commenter cette ligne si non nécessaire.
|
|
||||||
network_mode: host
|
network_mode: host
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
|
HOSTNAME: 0.0.0.0
|
||||||
|
DATABASE_PROVIDER: sqlite
|
||||||
|
DATABASE_URL: "file:/app/data/ipam.db"
|
||||||
|
RUN_SEED: "${RUN_SEED:-false}"
|
||||||
env_file:
|
env_file:
|
||||||
- ../.env
|
- ../.env
|
||||||
volumes:
|
volumes:
|
||||||
- ipam-data:/app/data
|
- ipam-data:/app/data
|
||||||
# Capabilities requises pour le ping ICMP raw
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_RAW
|
- NET_RAW
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
profiles: [sqlite, postgres]
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/ || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
profiles: ["sqlite"]
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Service IPAM — variante PostgreSQL
|
||||||
|
# =========================================================
|
||||||
|
ipam-pg:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
args:
|
||||||
|
DATABASE_PROVIDER: postgresql
|
||||||
|
image: ipam-homelab:postgres
|
||||||
|
container_name: ipam
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3000
|
||||||
|
HOSTNAME: 0.0.0.0
|
||||||
|
DATABASE_PROVIDER: postgresql
|
||||||
|
DATABASE_URL: "postgresql://ipam:${POSTGRES_PASSWORD:-ipam}@127.0.0.1:5432/ipam?schema=public"
|
||||||
|
RUN_SEED: "${RUN_SEED:-false}"
|
||||||
|
env_file:
|
||||||
|
- ../.env
|
||||||
|
cap_add:
|
||||||
|
- NET_RAW
|
||||||
|
- NET_ADMIN
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
required: false
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/ || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
profiles: ["postgres"]
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# PostgreSQL (profil "postgres" uniquement)
|
||||||
|
# =========================================================
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
container_name: ipam-postgres
|
container_name: ipam-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# network_mode host → expose le port 5432 directement sur l'hôte.
|
||||||
|
network_mode: host
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ipam
|
POSTGRES_USER: ipam
|
||||||
POSTGRES_PASSWORD: ipam
|
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-ipam}"
|
||||||
POSTGRES_DB: ipam
|
POSTGRES_DB: ipam
|
||||||
|
PGPORT: 5432
|
||||||
volumes:
|
volumes:
|
||||||
- ipam-postgres:/var/lib/postgresql/data
|
- ipam-postgres:/var/lib/postgresql/data
|
||||||
ports:
|
|
||||||
- '5432:5432'
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD-SHELL', 'pg_isready -U ipam']
|
test: ["CMD-SHELL", "pg_isready -U ipam -d ipam"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 10
|
||||||
profiles: [postgres]
|
profiles: ["postgres"]
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
ipam-data:
|
ipam-data:
|
||||||
|
driver: local
|
||||||
ipam-postgres:
|
ipam-postgres:
|
||||||
|
driver: local
|
||||||
|
|||||||
@@ -44,8 +44,15 @@ if [ "${DATABASE_PROVIDER:-sqlite}" = "sqlite" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 3. Migrations Prisma -----------------------------------------
|
# --- 3. Migrations Prisma -----------------------------------------
|
||||||
log "Application des migrations Prisma…"
|
# Si des migrations existent → migrate deploy (production-safe)
|
||||||
npx prisma migrate deploy
|
# Sinon (premier démarrage sans baseline) → db push pour initialiser le schéma
|
||||||
|
if [ -d /app/prisma/migrations ] && [ "$(find /app/prisma/migrations -mindepth 1 -maxdepth 1 -type d 2>/dev/null | head -n 1)" != "" ]; then
|
||||||
|
log "Application des migrations Prisma…"
|
||||||
|
npx prisma migrate deploy
|
||||||
|
else
|
||||||
|
log "Aucune migration trouvée — initialisation du schéma via 'prisma db push'…"
|
||||||
|
npx prisma db push --skip-generate --accept-data-loss
|
||||||
|
fi
|
||||||
|
|
||||||
# --- 4. Seed optionnel --------------------------------------------
|
# --- 4. Seed optionnel --------------------------------------------
|
||||||
if [ "${RUN_SEED:-false}" = "true" ]; then
|
if [ "${RUN_SEED:-false}" = "true" ]; then
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
|
// - native : dev local (macOS / Linux glibc)
|
||||||
|
// - linux-musl-openssl-3.0.x : runtime Alpine x86_64
|
||||||
|
// - linux-musl-arm64-openssl-3.0.x : runtime Alpine arm64 (Raspberry Pi, Apple Silicon en Docker, etc.)
|
||||||
|
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
@@ -49,10 +53,11 @@ model Host {
|
|||||||
vendor String? // Issue du MAC (OUI) ou d'une fingerprint
|
vendor String? // Issue du MAC (OUI) ou d'une fingerprint
|
||||||
osGuess String? // OS détecté / renseigné
|
osGuess String? // OS détecté / renseigné
|
||||||
|
|
||||||
status HostStatus @default(UNKNOWN)
|
// Valeurs : "UP" | "DOWN" | "UNKNOWN" (validation Zod côté app)
|
||||||
|
status String @default("UNKNOWN")
|
||||||
|
|
||||||
// Origine de la fiche : saisie manuelle ou découverte
|
// Origine de la fiche : "MANUAL" | "DISCOVERED" | "IMPORTED"
|
||||||
source HostSource @default(MANUAL)
|
source String @default("MANUAL")
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
networkId String?
|
networkId String?
|
||||||
@@ -71,17 +76,10 @@ model Host {
|
|||||||
@@index([status])
|
@@index([status])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HostStatus {
|
// NOTE : les enums Prisma ne sont pas supportés par SQLite. On stocke
|
||||||
UP
|
// donc tous les statuts/types en `String` et on valide les valeurs
|
||||||
DOWN
|
// possibles côté app via Zod. Constantes documentées au-dessus de
|
||||||
UNKNOWN
|
// chaque champ.
|
||||||
}
|
|
||||||
|
|
||||||
enum HostSource {
|
|
||||||
MANUAL
|
|
||||||
DISCOVERED
|
|
||||||
IMPORTED
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// Ports ouverts sur un hôte
|
// Ports ouverts sur un hôte
|
||||||
@@ -89,10 +87,12 @@ enum HostSource {
|
|||||||
model Port {
|
model Port {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
number Int
|
number Int
|
||||||
protocol Protocol @default(TCP)
|
// Valeurs : "TCP" | "UDP"
|
||||||
|
protocol String @default("TCP")
|
||||||
serviceName String? // ex: "http", "ssh", renseigné ou deviné
|
serviceName String? // ex: "http", "ssh", renseigné ou deviné
|
||||||
banner String? // Banner applicatif collecté
|
banner String? // Banner applicatif collecté
|
||||||
state PortState @default(OPEN)
|
// Valeurs : "OPEN" | "CLOSED" | "FILTERED" | "UNKNOWN"
|
||||||
|
state String @default("OPEN")
|
||||||
|
|
||||||
hostId String
|
hostId String
|
||||||
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||||
@@ -110,17 +110,8 @@ model Port {
|
|||||||
@@index([number])
|
@@index([number])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Protocol {
|
// Protocol : "TCP" | "UDP" — voir Port.protocol
|
||||||
TCP
|
// PortState : "OPEN" | "CLOSED" | "FILTERED" | "UNKNOWN" — voir Port.state
|
||||||
UDP
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PortState {
|
|
||||||
OPEN
|
|
||||||
CLOSED
|
|
||||||
FILTERED
|
|
||||||
UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// Applications (Jellyfin, Home Assistant, Nextcloud, etc.)
|
// Applications (Jellyfin, Home Assistant, Nextcloud, etc.)
|
||||||
@@ -160,8 +151,10 @@ model HostApplication {
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
model Scan {
|
model Scan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
type ScanType
|
// Valeurs : "PING" | "PORT" | "ARP" | "MDNS" | "FULL"
|
||||||
status ScanStatus @default(PENDING)
|
type String
|
||||||
|
// Valeurs : "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "CANCELLED"
|
||||||
|
status String @default("PENDING")
|
||||||
target String // CIDR, IP, ou hôte
|
target String // CIDR, IP, ou hôte
|
||||||
params String? // JSON (Zod validé côté app)
|
params String? // JSON (Zod validé côté app)
|
||||||
startedAt DateTime?
|
startedAt DateTime?
|
||||||
@@ -180,21 +173,8 @@ model Scan {
|
|||||||
@@index([type])
|
@@index([type])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ScanType {
|
// ScanType : "PING" | "PORT" | "ARP" | "MDNS" | "FULL" — voir Scan.type
|
||||||
PING
|
// ScanStatus : "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "CANCELLED" — voir Scan.status
|
||||||
PORT
|
|
||||||
ARP
|
|
||||||
MDNS
|
|
||||||
FULL
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ScanStatus {
|
|
||||||
PENDING
|
|
||||||
RUNNING
|
|
||||||
COMPLETED
|
|
||||||
FAILED
|
|
||||||
CANCELLED
|
|
||||||
}
|
|
||||||
|
|
||||||
// Résultat individuel d'un scan (rattaché à un hôte quand possible)
|
// Résultat individuel d'un scan (rattaché à un hôte quand possible)
|
||||||
model ScanResult {
|
model ScanResult {
|
||||||
|
|||||||
@@ -30,11 +30,13 @@ async function getHosts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusVariant = {
|
type BadgeVariant = 'default' | 'success' | 'warning' | 'destructive' | 'info' | 'outline';
|
||||||
|
|
||||||
|
const statusVariant: Record<string, BadgeVariant> = {
|
||||||
UP: 'success',
|
UP: 'success',
|
||||||
DOWN: 'destructive',
|
DOWN: 'destructive',
|
||||||
UNKNOWN: 'default',
|
UNKNOWN: 'default',
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
export default async function HostsPage() {
|
export default async function HostsPage() {
|
||||||
const hosts = await getHosts();
|
const hosts = await getHosts();
|
||||||
@@ -82,7 +84,7 @@ export default async function HostsPage() {
|
|||||||
{h.network?.name ?? '—'}
|
{h.network?.name ?? '—'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={statusVariant[h.status]}>{h.status}</Badge>
|
<Badge variant={statusVariant[h.status] ?? 'default'}>{h.status}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-muted-foreground">
|
<TableCell className="text-muted-foreground">
|
||||||
{h.ports.length > 0 ? (
|
{h.ports.length > 0 ? (
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ async function getScans() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusVariant = {
|
type BadgeVariant = 'default' | 'success' | 'warning' | 'destructive' | 'info' | 'outline';
|
||||||
|
|
||||||
|
const statusVariant: Record<string, BadgeVariant> = {
|
||||||
COMPLETED: 'success',
|
COMPLETED: 'success',
|
||||||
RUNNING: 'info',
|
RUNNING: 'info',
|
||||||
PENDING: 'default',
|
PENDING: 'default',
|
||||||
FAILED: 'destructive',
|
FAILED: 'destructive',
|
||||||
CANCELLED: 'warning',
|
CANCELLED: 'warning',
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
function formatDuration(start: Date | null, end: Date | null) {
|
function formatDuration(start: Date | null, end: Date | null) {
|
||||||
if (!start || !end) return '—';
|
if (!start || !end) return '—';
|
||||||
@@ -83,7 +85,7 @@ export default async function ScansPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{s.target}</TableCell>
|
<TableCell className="font-mono text-sm">{s.target}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={statusVariant[s.status]}>{s.status}</Badge>
|
<Badge variant={statusVariant[s.status] ?? 'default'}>{s.status}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="tabular-nums">{s.hostsFound}</TableCell>
|
<TableCell className="tabular-nums">{s.hostsFound}</TableCell>
|
||||||
<TableCell className="tabular-nums">{s.portsFound}</TableCell>
|
<TableCell className="tabular-nums">{s.portsFound}</TableCell>
|
||||||
|
|||||||
@@ -30,16 +30,14 @@ function parseIpNeigh(output: string): DiscoveryResult[] {
|
|||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((line) => {
|
.flatMap<DiscoveryResult>((line) => {
|
||||||
const ipMatch = line.match(/^(\d{1,3}(?:\.\d{1,3}){3})/);
|
const ipMatch = line.match(/^(\d{1,3}(?:\.\d{1,3}){3})/);
|
||||||
|
if (!ipMatch) return [];
|
||||||
const macMatch = line.match(/lladdr\s+([0-9a-fA-F:]{17})/);
|
const macMatch = line.match(/lladdr\s+([0-9a-fA-F:]{17})/);
|
||||||
if (!ipMatch) return null;
|
const result: DiscoveryResult = { ipAddress: ipMatch[1] };
|
||||||
return {
|
if (macMatch) result.macAddress = macMatch[1];
|
||||||
ipAddress: ipMatch[1],
|
return [result];
|
||||||
macAddress: macMatch?.[1],
|
});
|
||||||
} satisfies DiscoveryResult;
|
|
||||||
})
|
|
||||||
.filter((r): r is DiscoveryResult => r !== null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArpA(output: string): DiscoveryResult[] {
|
function parseArpA(output: string): DiscoveryResult[] {
|
||||||
@@ -48,14 +46,12 @@ function parseArpA(output: string): DiscoveryResult[] {
|
|||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((line) => {
|
.flatMap<DiscoveryResult>((line) => {
|
||||||
const ipMatch = line.match(/\((\d{1,3}(?:\.\d{1,3}){3})\)/);
|
const ipMatch = line.match(/\((\d{1,3}(?:\.\d{1,3}){3})\)/);
|
||||||
|
if (!ipMatch) return [];
|
||||||
const macMatch = line.match(/([0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5})/);
|
const macMatch = line.match(/([0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5})/);
|
||||||
if (!ipMatch) return null;
|
const result: DiscoveryResult = { ipAddress: ipMatch[1] };
|
||||||
return {
|
if (macMatch) result.macAddress = macMatch[1];
|
||||||
ipAddress: ipMatch[1],
|
return [result];
|
||||||
macAddress: macMatch?.[1],
|
});
|
||||||
} satisfies DiscoveryResult;
|
|
||||||
})
|
|
||||||
.filter((r): r is DiscoveryResult => r !== null);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,36 @@
|
|||||||
import { Bonjour } from 'bonjour-service';
|
import { Bonjour, type Service } from 'bonjour-service';
|
||||||
import { env } from '@/config/env';
|
import { env } from '@/config/env';
|
||||||
import type { DiscoveryResult, DiscoveryTarget, Scanner } from '../types';
|
import type { DiscoveryResult, DiscoveryTarget, Scanner } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Découverte via mDNS / DNS-SD (_services._dns-sd._udp.local).
|
* Découverte via mDNS / DNS-SD.
|
||||||
* Écoute pendant `timeout` ms et agrège les annonces reçues.
|
* `bonjour-service` exige un `type` par browser : on en lance un par type
|
||||||
|
* courant en homelab et on agrège les annonces reçues pendant `timeout` ms.
|
||||||
*/
|
*/
|
||||||
const DEFAULT_TIMEOUT_MS = 4000;
|
const DEFAULT_TIMEOUT_MS = 4000;
|
||||||
|
|
||||||
|
// Types mDNS les plus fréquents en homelab (sans le préfixe `_` ni le suffixe `._tcp/._udp`)
|
||||||
|
const COMMON_SERVICE_TYPES: Array<{ type: string; protocol: 'tcp' | 'udp' }> = [
|
||||||
|
{ type: 'http', protocol: 'tcp' },
|
||||||
|
{ type: 'https', protocol: 'tcp' },
|
||||||
|
{ type: 'ssh', protocol: 'tcp' },
|
||||||
|
{ type: 'sftp-ssh', protocol: 'tcp' },
|
||||||
|
{ type: 'smb', protocol: 'tcp' },
|
||||||
|
{ type: 'nfs', protocol: 'tcp' },
|
||||||
|
{ type: 'afpovertcp', protocol: 'tcp' },
|
||||||
|
{ type: 'workstation', protocol: 'tcp' },
|
||||||
|
{ type: 'device-info', protocol: 'tcp' },
|
||||||
|
{ type: 'printer', protocol: 'tcp' },
|
||||||
|
{ type: 'ipp', protocol: 'tcp' },
|
||||||
|
{ type: 'ipps', protocol: 'tcp' },
|
||||||
|
{ type: 'airplay', protocol: 'tcp' },
|
||||||
|
{ type: 'raop', protocol: 'tcp' },
|
||||||
|
{ type: 'googlecast', protocol: 'tcp' },
|
||||||
|
{ type: 'homekit', protocol: 'tcp' },
|
||||||
|
{ type: 'hap', protocol: 'tcp' },
|
||||||
|
{ type: 'spotify-connect', protocol: 'tcp' },
|
||||||
|
];
|
||||||
|
|
||||||
export const mdnsScanner: Scanner = {
|
export const mdnsScanner: Scanner = {
|
||||||
kind: 'mdns',
|
kind: 'mdns',
|
||||||
label: 'mDNS / Bonjour',
|
label: 'mDNS / Bonjour',
|
||||||
@@ -17,11 +40,14 @@ export const mdnsScanner: Scanner = {
|
|||||||
const bonjour = new Bonjour();
|
const bonjour = new Bonjour();
|
||||||
const byIp = new Map<string, DiscoveryResult>();
|
const byIp = new Map<string, DiscoveryResult>();
|
||||||
|
|
||||||
const browser = bonjour.find({});
|
const ingest = (svc: Service) => {
|
||||||
browser.on('up', (svc) => {
|
|
||||||
const ips = (svc.addresses ?? []).filter((a) => a && !a.includes(':')); // IPv4
|
const ips = (svc.addresses ?? []).filter((a) => a && !a.includes(':')); // IPv4
|
||||||
for (const ip of ips) {
|
for (const ip of ips) {
|
||||||
const existing = byIp.get(ip) ?? { ipAddress: ip, hostname: svc.host, services: [] };
|
const existing = byIp.get(ip) ?? {
|
||||||
|
ipAddress: ip,
|
||||||
|
hostname: svc.host,
|
||||||
|
services: [],
|
||||||
|
};
|
||||||
existing.services!.push({
|
existing.services!.push({
|
||||||
name: svc.name,
|
name: svc.name,
|
||||||
type: `_${svc.type}._${svc.protocol}.local`,
|
type: `_${svc.type}._${svc.protocol}.local`,
|
||||||
@@ -29,10 +55,16 @@ export const mdnsScanner: Scanner = {
|
|||||||
});
|
});
|
||||||
byIp.set(ip, existing);
|
byIp.set(ip, existing);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const browsers = COMMON_SERVICE_TYPES.map(({ type, protocol }) => {
|
||||||
|
const browser = bonjour.find({ type, protocol });
|
||||||
|
browser.on('up', ingest);
|
||||||
|
return browser;
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, DEFAULT_TIMEOUT_MS));
|
await new Promise((r) => setTimeout(r, DEFAULT_TIMEOUT_MS));
|
||||||
browser.stop();
|
for (const b of browsers) b.stop();
|
||||||
bonjour.destroy();
|
bonjour.destroy();
|
||||||
|
|
||||||
return [...byIp.values()];
|
return [...byIp.values()];
|
||||||
|
|||||||
Reference in New Issue
Block a user