kvm/pkg/nmlite/resolvconf.go

198 lines
5.4 KiB
Go

package nmlite
import (
"bytes"
"fmt"
"html/template"
"net"
"os"
"strings"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/rs/zerolog"
)
const (
resolvConfPath = "/etc/resolv.conf"
resolvConfFileMode = 0644
resolvConfTemplate = `# the resolv.conf file is managed by the jetkvm network manager
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
{{ if .searchList }}
search {{ join .searchList " " }} # {{ .iface }}
{{- end -}}
{{ if .domain }}
domain {{ .domain }} # {{ .iface }}
{{- end -}}
{{ range .nameservers }}
nameserver {{ printf "%s" . }} # {{ $.iface }}
{{- end }}
`
)
var (
tplFuncMap = template.FuncMap{
"join": strings.Join,
}
)
// ResolvConfManager manages the resolv.conf file
type ResolvConfManager struct {
logger *zerolog.Logger
}
// NewResolvConfManager creates a new resolv.conf manager
func NewResolvConfManager(logger *zerolog.Logger) *ResolvConfManager {
if logger == nil {
// Create a no-op logger if none provided
logger = &zerolog.Logger{}
}
return &ResolvConfManager{
logger: logger,
}
}
// UpdateFromLease updates resolv.conf from a DHCP lease
func (rcm *ResolvConfManager) UpdateFromLease(lease *types.DHCPLease) error {
if lease == nil {
return fmt.Errorf("lease cannot be nil")
}
rcm.logger.Info().
Str("interface", lease.InterfaceName).
Msg("updating resolv.conf from DHCP lease")
return rcm.Update(lease.InterfaceName, lease.DNS, lease.SearchList, lease.Domain)
}
// UpdateFromStaticConfig updates resolv.conf from static configuration
func (rcm *ResolvConfManager) UpdateFromStaticConfig(iface string, dns []string) error {
if len(dns) == 0 {
rcm.logger.Debug().Str("interface", iface).Msg("no DNS servers in static config")
return nil
}
// Parse DNS servers
var dnsIPs []net.IP
for _, dnsStr := range dns {
dnsIP := net.ParseIP(dnsStr)
if dnsIP == nil {
rcm.logger.Warn().Str("dns", dnsStr).Msg("invalid DNS server, skipping")
continue
}
dnsIPs = append(dnsIPs, dnsIP)
}
if len(dnsIPs) == 0 {
rcm.logger.Debug().Str("interface", iface).Msg("no valid DNS servers in static config")
return nil
}
rcm.logger.Info().
Str("interface", iface).
Interface("dns", dnsIPs).
Msg("updating resolv.conf from static config")
return rcm.Update(iface, dnsIPs, nil, "")
}
// Update updates the resolv.conf file
func (rcm *ResolvConfManager) Update(iface string, nameservers []net.IP, searchList []string, domain string) error {
rcm.logger.Debug().
Str("interface", iface).
Interface("nameservers", nameservers).
Interface("searchList", searchList).
Str("domain", domain).
Msg("updating resolv.conf")
// Generate resolv.conf content
content, err := rcm.generateResolvConf(iface, nameservers, searchList, domain)
if err != nil {
return fmt.Errorf("failed to generate resolv.conf: %w", err)
}
// Write to file
if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
return fmt.Errorf("failed to write resolv.conf: %w", err)
}
rcm.logger.Info().
Str("interface", iface).
Int("nameservers", len(nameservers)).
Msg("resolv.conf updated successfully")
return nil
}
// generateResolvConf generates resolv.conf content
func (rcm *ResolvConfManager) generateResolvConf(iface string, nameservers []net.IP, searchList []string, domain string) ([]byte, error) {
tmpl, err := template.New("resolv.conf").Funcs(tplFuncMap).Parse(resolvConfTemplate)
if err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, map[string]interface{}{
"iface": iface,
"nameservers": nameservers,
"searchList": searchList,
"domain": domain,
}); err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
}
return buf.Bytes(), nil
}
// Clear clears the resolv.conf file (removes all entries)
func (rcm *ResolvConfManager) Clear() error {
rcm.logger.Info().Msg("clearing resolv.conf")
content := []byte("# the resolv.conf file is managed by the jetkvm network manager\n# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN\n")
if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
return fmt.Errorf("failed to clear resolv.conf: %w", err)
}
rcm.logger.Info().Msg("resolv.conf cleared")
return nil
}
// GetCurrentContent returns the current content of resolv.conf
func (rcm *ResolvConfManager) GetCurrentContent() ([]byte, error) {
return os.ReadFile(resolvConfPath)
}
// Backup creates a backup of the current resolv.conf
func (rcm *ResolvConfManager) Backup() error {
content, err := rcm.GetCurrentContent()
if err != nil {
return fmt.Errorf("failed to read current resolv.conf: %w", err)
}
backupPath := resolvConfPath + ".backup"
if err := os.WriteFile(backupPath, content, resolvConfFileMode); err != nil {
return fmt.Errorf("failed to create backup: %w", err)
}
rcm.logger.Info().Str("backup", backupPath).Msg("resolv.conf backed up")
return nil
}
// Restore restores resolv.conf from backup
func (rcm *ResolvConfManager) Restore() error {
backupPath := resolvConfPath + ".backup"
content, err := os.ReadFile(backupPath)
if err != nil {
return fmt.Errorf("failed to read backup: %w", err)
}
if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
return fmt.Errorf("failed to restore resolv.conf: %w", err)
}
rcm.logger.Info().Str("backup", backupPath).Msg("resolv.conf restored from backup")
return nil
}