feat(wireguard): selektiver .conf-Import — Auswahl einzelner Interfaces per Modal
- GET /wireguard/importable listet alle .conf-Dateien in /etc/wireguard/ mit already_in-Flag (bereits in DB) - POST /wireguard/import nimmt optionale Names-Liste; ohne Namen → alles - edgeguard-ctl wg-import [iface…] importiert nur die genannten Interfaces - UI: Checkbox-Modal mit "Alle neuen auswählen" + Einzelauswahl; bereits importierte Interfaces disabled + grüner Tag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,8 @@ func (h *WireguardHandler) Register(rg *gin.RouterGroup) {
|
||||
// row per (iface, peer) with last_handshake + transfer counters.
|
||||
// Polled by the UI every 10s; no DB write.
|
||||
g.GET("/status", h.Status)
|
||||
g.GET("/importable", h.ListImportable)
|
||||
g.POST("/import", h.Import)
|
||||
}
|
||||
|
||||
// ── Live wg-show status ─────────────────────────────────────────────
|
||||
@@ -142,6 +144,45 @@ func (h *WireguardHandler) Status(c *gin.Context) {
|
||||
response.OK(c, gin.H{"status": rows})
|
||||
}
|
||||
|
||||
// ── Import ────────────────────────────────────────────────────────
|
||||
|
||||
const wgImportDir = "/etc/wireguard"
|
||||
|
||||
// ListImportable scans /etc/wireguard for *.conf files and reports
|
||||
// which are new (not yet in the DB) vs. already imported.
|
||||
func (h *WireguardHandler) ListImportable(c *gin.Context) {
|
||||
im := wireguard.NewImporter(h.Ifaces, h.Peers, h.Box)
|
||||
entries, err := im.ListImportable(c.Request.Context(), wgImportDir)
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"entries": entries})
|
||||
}
|
||||
|
||||
// Import imports selected (or all) WireGuard .conf files from
|
||||
// /etc/wireguard into the DB. Body: { "names": ["wg0","wg1"] }.
|
||||
// Empty or missing names → import all.
|
||||
func (h *WireguardHandler) Import(c *gin.Context) {
|
||||
var req struct {
|
||||
Names []string `json:"names"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
im := wireguard.NewImporter(h.Ifaces, h.Peers, h.Box)
|
||||
res, err := im.ImportSelected(c.Request.Context(), wgImportDir, req.Names)
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "wireguard.import",
|
||||
fmt.Sprintf("%d ifaces", res.IfacesAdded), res, h.NodeID)
|
||||
h.reload(c.Request.Context(), "import")
|
||||
response.OK(c, res)
|
||||
}
|
||||
|
||||
// ── Keygen ────────────────────────────────────────────────────────
|
||||
|
||||
func (h *WireguardHandler) GenerateKeypair(c *gin.Context) {
|
||||
|
||||
@@ -47,6 +47,68 @@ func NewImporter(ifaces *InterfacesRepo, peers *PeersRepo, box *secrets.Box) *Im
|
||||
return &Importer{Ifaces: ifaces, Peers: peers, Box: box}
|
||||
}
|
||||
|
||||
// ImportableEntry describes one .conf file that can be imported.
|
||||
type ImportableEntry struct {
|
||||
Name string `json:"name"` // interface name without .conf
|
||||
AlreadyIn bool `json:"already_in"` // already present in the DB
|
||||
}
|
||||
|
||||
// ListImportable scans dir for *.conf files and reports which are new
|
||||
// (not yet in the DB) and which are already imported.
|
||||
func (im *Importer) ListImportable(ctx context.Context, dir string) ([]ImportableEntry, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
existing, err := im.Ifaces.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inDB := make(map[string]bool, len(existing))
|
||||
for _, x := range existing {
|
||||
inDB[x.Name] = true
|
||||
}
|
||||
var out []ImportableEntry
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".conf") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(e.Name(), ".conf")
|
||||
if !validIfaceName(name) {
|
||||
continue
|
||||
}
|
||||
out = append(out, ImportableEntry{Name: name, AlreadyIn: inDB[name]})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ImportSelected imports only the named interfaces from dir.
|
||||
// An empty names slice is treated as "import all" (same as ImportDir).
|
||||
func (im *Importer) ImportSelected(ctx context.Context, dir string, names []string) (*ImportResult, error) {
|
||||
if len(names) == 0 {
|
||||
return im.ImportDir(ctx, dir)
|
||||
}
|
||||
res := &ImportResult{}
|
||||
want := make(map[string]bool, len(names))
|
||||
for _, n := range names {
|
||||
want[n] = true
|
||||
}
|
||||
for name := range want {
|
||||
if !validIfaceName(name) {
|
||||
res.Skipped = append(res.Skipped, name+" (invalid name)")
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, name+".conf")
|
||||
if err := im.importFile(ctx, name, path, res); err != nil {
|
||||
res.Skipped = append(res.Skipped, name+": "+err.Error())
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (im *Importer) ImportDir(ctx context.Context, dir string) (*ImportResult, error) {
|
||||
res := &ImportResult{}
|
||||
entries, err := os.ReadDir(dir)
|
||||
|
||||
Reference in New Issue
Block a user