//  Copyright 2024 Google LLC
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

package manager

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"slices"
	"strings"

	"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg"
	"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/osinfo"
	"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/run"
	"github.com/GoogleCloudPlatform/guest-agent/utils"
	"github.com/GoogleCloudPlatform/guest-logging-go/logger"
)

const (
	// netplanEthernetSuffix is the ethernet drop-in's file suffix.
	netplanEthernetSuffix = "ethernet"

	// netplanVlanSuffix is the vlan drop-in's file suffix.
	netplanVlanSuffix = "vlan"

	// netplanConfigVersion defines the version we are using for netplan's drop-in
	// files.
	netplanConfigVersion = 2
)

// netplan is the netplan's Service interface implementation. From the guest agent's
// network manager perspective the current form only supports the
// systemd-networkd + netplan combination. Since netplan also supports NetworkManager
// as a backend we'll eventually support such a combination in the future.
type netplan struct {
	// netplanConfigDir determines where the agent writes netplan configuration files.
	netplanConfigDir string

	// networkdDropinDir determines where the agent writes the networkd configuration files.
	networkdDropinDir string

	// priority dictates the priority with which guest-agent should write
	// the configuration files.
	priority int

	// interfacePrefix is prefix used to override default netplan config. This prefix is
	// used with netplan interface config keys in /run/netplan/20-google-guest-agent-ethernet.yaml
	// and systemd drop-in directory name like /etc/systemd/network/10-netplan-a-ens4.network.d/
	interfacePrefix string
}

// netplanDropin maps the netplan dropin configuration yaml entries/data
// structure.
type netplanDropin struct {
	Network netplanNetwork `yaml:"network"`
}

// netplanNetwork is the netplan's drop-in network section.
type netplanNetwork struct {
	// Version is the netplan's drop-in format version.
	Version int `yaml:"version"`

	// Ethernets are the ethernet configuration entries map.
	Ethernets map[string]netplanEthernet `yaml:"ethernets,omitempty"`

	// Vlans are the vlan interface's configuration entries map.
	Vlans map[string]netplanVlan `yaml:"vlans,omitempty"`
}

// netplanEthernet describes the actual ethernet configuration. Refer
// https://netplan.readthedocs.io/en/stable/netplan-yaml/#properties-for-device-type-ethernets
// for more details.
type netplanEthernet struct {
	// Match is the interface's matching rule.
	Match netplanMatch `yaml:"match"`

	// MTU defines the interface's MTU configuration.
	MTU *int

	// DHCPv4 determines if DHCPv4 support must be enabled to such an interface.
	DHCPv4 *bool `yaml:"dhcp4,omitempty"`

	DHCP4Overrides *netplanDHCPOverrides `yaml:"dhcp4-overrides,omitempty"`

	// DHCPv6 determines if DHCPv6 support must be enabled to such an interface.
	DHCPv6 *bool `yaml:"dhcp6,omitempty"`

	DHCP6Overrides *netplanDHCPOverrides `yaml:"dhcp6-overrides,omitempty"`
}

// netplanDHCPOverrides sets the netplan dhcp-overrides configuration.
type netplanDHCPOverrides struct {
	// When true, the domain name received from the DHCP server will be used as DNS
	// search domain over this link.
	UseDomains *bool `yaml:"use-domains,omitempty"`
}

// netplanMatch contains the keys uses to match an interface.
type netplanMatch struct {
	// Name is the key used to match an interface by its name.
	Name string `yaml:"name"`
}

// netplanVlan describes the netplan's vlan interface configuration.
// Refer https://netplan.readthedocs.io/en/stable/netplan-yaml/#properties-for-device-type-vlans
// for more details.
type netplanVlan struct {
	// ID is the the VLAN ID.
	ID int `yaml:"id,omitempty"`

	// Link is the vlan's parent interface.
	Link string `yaml:"link,omitempty"`

	// DHCPv4 determines if DHCPv4 support must be enabled to such an interface.
	DHCPv4 *bool `yaml:"dhcp4,omitempty"`

	// DHCPv6 determines if DHCPv6 support must be enabled to such an interface.
	DHCPv6 *bool `yaml:"dhcp6,omitempty"`

	// OverrideMacAddress sets the device’s MAC address. By default it will use same as
	// parent NIC.
	OverrideMacAddress string `yaml:"macaddress,omitempty"`

	// MTU sets the MTU for interface. The default is 1500.
	MTU int `yaml:"mtu,omitempty"`

	// DHCP4Overrides sets DHCP4 overrides for the vlan.
	DHCP4Overrides *netplanDHCPOverrides `yaml:"dhcp4-overrides,omitempty"`

	// DHCP6Overrides sets DHCP6 overrides for the vlan.
	DHCP6Overrides *netplanDHCPOverrides `yaml:"dhcp6-overrides,omitempty"`
}

// networkdNetplanDropin maps systemd-networkd's overriding drop-in if networkd
// is present.
type networkdNetplanDropin struct {
	// Match is the systemd-networkd ini file's [Match] section.
	Match systemdMatchConfig

	// Network is the systemd-networkd ini file's [Network] section.
	Network systemdNetworkConfig `ini:"Network"`

	// DHCPv4 is the systemd-networkd ini file's [DHCPv4] section.
	DHCPv4 *systemdDHCPConfig `ini:",omitempty"`
}

// Name returns the name of the network manager service.
func (n *netplan) Name() string {
	return "netplan"
}

// Configure gives the opportunity for the Service implementation to adjust its configuration
// based on the Guest Agent configuration.
func (n *netplan) Configure(ctx context.Context, config *cfg.Sections) {
	os := osinfo.Get()
	// Debian 12 has a pretty generic matching netplan configuration for gce,
	// regex in /etc/netplan/90-default.yaml matches all en* and eth* nics.
	// Until we have that changed we are adjusting the configuration so we can
	// override the defaults.
	if os.OS == "debian" && os.Version.Major == 12 {
		n.interfacePrefix = "a"
		logger.Infof("Setting up Debian 12, overriding interface prefix with: %q", n.interfacePrefix)
	}
}

// IsManaging checks whether netplan is present in the system.
func (n *netplan) IsManaging(ctx context.Context, iface string) (bool, error) {
	if isUbuntu1804() {
		logger.Infof("Running on Ubuntu 18.04, skipping use of netplan, falling back to dhclient")
		return false, nil
	}

	// Check if the netplan CLI exists.
	return cliExists("netplan")
}

// SetupEthernetInterface sets the network interfaces for netplan by writing drop-in files to the specified
// configuration directory.
func (n *netplan) SetupEthernetInterface(ctx context.Context, config *cfg.Sections, nics *Interfaces) error {
	// Create a network configuration file with default configurations for each network interface.
	googleInterfaces, googleIpv6Interfaces := interfaceListsIpv4Ipv6(nics.EthernetInterfaces)

	mtuMap, err := interfacesMTUMap(nics.EthernetInterfaces)
	if err != nil {
		return fmt.Errorf("error listing interface's MTU configuration: %w", err)
	}

	// Write the config files.
	reload1, err := n.writeNetplanEthernetDropin(mtuMap, googleInterfaces, googleIpv6Interfaces)
	if err != nil {
		return fmt.Errorf("error writing network configs: %v", err)
	}

	// If we are running netplan+systemd-networkd we try to write networkd's drop-in for configs
	// not mapped/supported by netplan.
	reload2, err := n.writeNetworkdDropin(googleInterfaces, googleIpv6Interfaces)
	if err != nil {
		return fmt.Errorf("error writing systemd-networkd's drop-in: %v", err)
	}

	// Avoid unnecessary reloads, if we've really updated some config then only do a reload.
	if reload1 || reload2 {
		if err := n.reloadConfigs(ctx); err != nil {
			return fmt.Errorf("error applying ethernet interface configs: %w", err)
		}
	}

	return nil
}

// reloadConfigs triggers config reload to make sure ethernet/vlan configs are written
// on disk are applied by netplan.
func (n *netplan) reloadConfigs(ctx context.Context) error {
	logger.Infof("Reloading netplan configs...")

	// Avoid restarting netplan.
	if err := run.Quiet(ctx, "netplan", "generate"); err != nil {
		return fmt.Errorf("error generating netplan based config: %w", err)
	}

	// Avoid restarting systemd-networkd.
	if err := run.Quiet(ctx, "networkctl", "reload"); err != nil {
		return fmt.Errorf("error reloading systemd-networkd network configs: %v", err)
	}

	return nil
}

// writeNetworkdDropin writes the overloading network-manager's drop-in file for the configurations
// not supported by netplan.
func (n *netplan) writeNetworkdDropin(interfaces, ipv6Interfaces []string) (bool, error) {
	var requiresReload bool
	stat, err := os.Stat(n.networkdDropinDir)
	if err != nil {
		return false, fmt.Errorf("failed to stat systemd-networkd's drop-in root dir: %w", err)
	}

	if !stat.IsDir() {
		return false, fmt.Errorf("systemd-networkd drop-in dir(%s) is not a dir", n.networkdDropinDir)
	}

	for i, iface := range interfaces {
		if !shouldManageInterface(i == 0) {
			logger.Debugf("ManagePrimaryNIC is disabled, skipping writeNetworkdDropin for %s", iface)
			continue
		}
		logger.Debugf("writing systemd-networkd drop-in config for %s", iface)

		var dhcp = "ipv4"
		if slices.Contains(ipv6Interfaces, iface) {
			dhcp = "yes"
		}

		// Create and setup ini file.
		data := networkdNetplanDropin{
			Match: systemdMatchConfig{
				Name: iface,
			},
			Network: systemdNetworkConfig{
				DNSDefaultRoute: true,
				DHCP:            dhcp,
			},
		}

		// We are only interested on DHCP offered routes on the primary nic,
		// ignore it for the secondary ones.
		if i != 0 {
			data.Network.DNSDefaultRoute = false
			data.DHCPv4 = &systemdDHCPConfig{
				RoutesToDNS: false,
				RoutesToNTP: false,
			}
		}

		wrote, err := data.write(n, iface)
		if err != nil {
			return false, fmt.Errorf("failed to write systemd drop-in config: %w", err)
		}

		if wrote {
			requiresReload = true
		}
	}

	return requiresReload, nil
}

// networkdDropinFile returns an interface's netplan drop-in file path.
func (n *netplan) networkdDropinFile(iface string) string {
	// We are hardcoding the netplan priority to 10 since we are deriving the netplan
	// networkd configuration name based on the interface name only - aligning with
	// the commonly used value for netplan.
	if n.interfacePrefix != "" {
		return filepath.Join(n.networkdDropinDir, fmt.Sprintf("10-netplan-%s-%s.network.d", n.interfacePrefix, iface), "override.conf")
	}

	return filepath.Join(n.networkdDropinDir, fmt.Sprintf("10-netplan-%s.network.d", iface), "override.conf")
}

// isSame unmarshals netplan networkd dropin config from cfgFile and compares it with
// own instance. If it fails to read it returns false to allow caller to try
// overwriting and fix any issues if file already exists.
func (nd networkdNetplanDropin) isSame(cfgFile string) bool {
	existingCfgs := networkdNetplanDropin{}
	if err := readIniFile(cfgFile, &existingCfgs); err != nil {
		logger.Debugf("Failed to read %q while comparing netplan networkd dropins with error: %v", cfgFile, err)
		return false
	}

	return reflect.DeepEqual(nd, existingCfgs)
}

// write writes systemd's drop-in config file.
func (nd networkdNetplanDropin) write(n *netplan, iface string) (bool, error) {
	dropinFile := n.networkdDropinFile(iface)

	logger.Infof("writing systemd drop in to: %s", dropinFile)

	dropinDir := filepath.Dir(dropinFile)
	if err := os.MkdirAll(dropinDir, 0755); err != nil {
		return false, fmt.Errorf("failed to create networkd dropin dir: %w", err)
	}

	if nd.isSame(dropinFile) {
		logger.Infof("Exact same config already exists at location %q, skipping overwriting to avoid network reload", dropinFile)
		return false, nil
	}

	if err := writeIniFile(dropinFile, &nd); err != nil {
		return false, fmt.Errorf("error saving netword drop-in file for %s: %v", iface, err)
	}

	return true, nil
}

// shouldUseDomains returns true if interface index is 0.
func shouldUseDomains(idx int) *bool {
	res := idx == 0
	return &res
}

// writeNetplanEthernetDropin selects the ethernet configuration, transforms it
// into a netplan dropin format and writes it down to the netplan's drop-in directory.
func (n *netplan) writeNetplanEthernetDropin(mtuMap map[string]int, interfaces, ipv6Interfaces []string) (bool, error) {
	dropin := netplanDropin{
		Network: netplanNetwork{
			Version:   netplanConfigVersion,
			Ethernets: make(map[string]netplanEthernet),
		},
	}

	for i, iface := range interfaces {
		if !shouldManageInterface(i == 0) {
			logger.Debugf("ManagePrimaryNIC is disabled, skipping writeNetplanEthernetDropin for %s", iface)
			continue
		}
		logger.Debugf("Adding %s(%d) to drop-in configuration.", iface, i)

		trueVal := true
		ne := netplanEthernet{
			Match:  netplanMatch{Name: iface},
			DHCPv4: &trueVal,
			DHCP4Overrides: &netplanDHCPOverrides{
				UseDomains: shouldUseDomains(i),
			},
		}

		if mtu, found := mtuMap[iface]; found {
			ne.MTU = &mtu
		}

		if slices.Contains(ipv6Interfaces, iface) {
			ne.DHCPv6 = &trueVal
			ne.DHCP6Overrides = &netplanDHCPOverrides{
				UseDomains: shouldUseDomains(i),
			}
		}

		key := n.ID(iface)
		dropin.Network.Ethernets[key] = ne
	}

	// This can happen if its a single NIC VM and primary NIC is not managed
	// by Guest Agent. No need to write a file with just version in [dropin].
	if len(dropin.Network.Ethernets) == 0 {
		logger.Infof("No NICs to configure, skipping writeNetplanEthernetDropin")
		return false, nil
	}

	wrote, err := n.write(dropin, netplanEthernetSuffix)
	if err != nil {
		return false, fmt.Errorf("failed to write netplan ethernet drop-in config: %+v", err)
	}

	return wrote, nil
}

// ID returns the Netplan ID used for referencing parent NIC in VLAN NIC
// configuration and the key in ethernet based NIC configuration.
func (n *netplan) ID(iface string) string {
	key := iface
	if n.interfacePrefix != "" {
		key = fmt.Sprintf("%s-%s", n.interfacePrefix, iface)
	}
	return key
}

// isSame unmarshals netplan dropin config from cfgFile and compares it with
// own instance. If it fails to read it returns false to allow caller to try
// overwriting and fix any issues if file already exists.
func (nd netplanDropin) isSame(cfgFile string) bool {
	existingDropin := netplanDropin{}
	if err := readYamlFile(cfgFile, &existingDropin); err != nil {
		logger.Debugf("Failed to read %q while comparing netplan dropins with error: %v", cfgFile, err)
		return false
	}

	return reflect.DeepEqual(nd, existingDropin)
}

// write writes the netplan dropin file.
func (n *netplan) write(nd netplanDropin, suffix string) (bool, error) {
	dropinFile := n.dropinFile(suffix)
	dropinDir := filepath.Dir(dropinFile)
	err := os.MkdirAll(dropinDir, 0755)
	if err != nil {
		return false, fmt.Errorf("failed to create networkd dropin dir: %w", err)
	}

	if nd.isSame(dropinFile) {
		logger.Infof("Exact same config already exists at location %q, skipping overwriting to avoid network reload", dropinFile)
		return false, nil
	}

	if err := writeYamlFile(dropinFile, &nd); err != nil {
		return false, fmt.Errorf("error saving netplan drop-in file %s: %w", dropinFile, err)
	}
	return true, nil
}

// dropinFile returns the netplan drop-in file.
// Priority is lexicographically sorted in ascending order by file name. So a configuration
// starting with '1-' takes priority over a configuration file starting with '10-'. Setting
// a priority of 1 allows the guest-agent to override any existing default configurations
// while also allowing users the freedom of using priorities of '0...' to override the
// agent's own configurations.
func (n *netplan) dropinFile(suffix string) string {
	return filepath.Join(n.netplanConfigDir, fmt.Sprintf("%d-google-guest-agent-%s.yaml", n.priority, suffix))
}

func (n *netplan) vlanInterfaceName(parentInterface string, vlanID int) string {
	return fmt.Sprintf("gcp.%s.%d", parentInterface, vlanID)
}

// SetupVlanInterface writes the apppropriate vLAN interfaces netplan configuration.
func (n *netplan) SetupVlanInterface(ctx context.Context, config *cfg.Sections, nics *Interfaces) error {
	toRemove, err := n.findVlanDiff(nics)
	if err != nil {
		return fmt.Errorf("unable to detect vlan nics to delete: %w", err)
	}

	reload1, err := n.rollbackVlanNics(ctx, toRemove)
	if err != nil {
		return fmt.Errorf("unable to remove vlan interfaces (%+v): %w", toRemove, err)
	}

	reload2, err := n.writeNetplanVLANDropin(nics)
	if err != nil {
		return fmt.Errorf("unable to write netplan VLAN dropin: %w", err)
	}

	reload3, err := n.writeNetworkdVLANDropin(nics)
	if err != nil {
		return fmt.Errorf("unable to write netplan networkd VLAN dropin: %w", err)
	}

	if reload1 || reload2 || reload3 {
		if err := n.reloadConfigs(ctx); err != nil {
			return fmt.Errorf("error applying vlan interface configs: %w", err)
		}
	}

	return nil
}

// interfaceFromLink gets the interface name from link name in netplan config.
// Link name in some cases might not be same as interface name as we prefix with "a"
// for precedence.
func (n *netplan) interfaceFromLink(link string) string {
	iface := link
	if n.interfacePrefix != "" {
		iface = strings.TrimPrefix(iface, fmt.Sprintf("%s-", n.interfacePrefix))
	}
	return iface
}

// findVlanDiff compares expectedNics with one configured with netplan config on disk
// and returns only the vlan interfaces to delete.
func (n *netplan) findVlanDiff(expectedNics *Interfaces) (*Interfaces, error) {
	keepInterfaces := make(map[string]string)
	toRemove := &Interfaces{VlanInterfaces: make(map[int]VlanInterface)}

	existingVlanCfgs := netplanDropin{}
	netplanVlanDropinFile := n.dropinFile(netplanVlanSuffix)
	// There's no config file per interface, single netplan config file lists all the interfaces.
	if !utils.FileExists(netplanVlanDropinFile, utils.TypeFile) {
		logger.Infof("File %q does not exist, nothing to rollback", netplanVlanDropinFile)
		return toRemove, nil
	}

	if err := readYamlFile(netplanVlanDropinFile, &existingVlanCfgs); err != nil {
		return toRemove, fmt.Errorf("unable to read %q trying rollback configs: %w", netplanVlanDropinFile, err)
	}

	if len(existingVlanCfgs.Network.Vlans) == 0 {
		logger.Debugf("No existing VLAN configs found at %q, skipping rollback", netplanVlanDropinFile)
		return toRemove, nil
	}

	for _, iface := range expectedNics.VlanInterfaces {
		// Set netplan vlan drop-in file for removal.
		ifaceName := n.vlanInterfaceName(iface.ParentInterfaceID, iface.Vlan)
		key := n.ID(ifaceName)
		keepInterfaces[key] = ifaceName
	}

	for vlanKey, vlan := range existingVlanCfgs.Network.Vlans {
		_, ok := keepInterfaces[vlanKey]
		if !ok {
			toRemove.VlanInterfaces[vlan.ID] = VlanInterface{ParentInterfaceID: n.interfaceFromLink(vlan.Link)}
		}
	}

	return toRemove, nil
}

// rollbackVlanNics removes the [nics] and its config (netplan and networkd dropin both) on disk.
func (n *netplan) rollbackVlanNics(ctx context.Context, nics *Interfaces) (bool, error) {
	var deleteNics []string
	var deleteDirs []string

	if len(nics.VlanInterfaces) == 0 {
		logger.Debugf("No VLAN interfaces in args, skipping rollback")
		return false, nil
	}

	existingVlanCfgs := netplanDropin{}
	netplanVlanDropinFile := n.dropinFile(netplanVlanSuffix)
	// There's no config file per interface, single netplan config file lists all the interfaces.
	if utils.FileExists(netplanVlanDropinFile, utils.TypeFile) {
		if err := readYamlFile(netplanVlanDropinFile, &existingVlanCfgs); err != nil {
			return false, fmt.Errorf("unable to read %q trying rollback configs: %w", netplanVlanDropinFile, err)
		}
	}

	for id, iface := range nics.VlanInterfaces {
		ifaceName := n.vlanInterfaceName(iface.ParentInterfaceID, id)
		key := n.ID(ifaceName)

		deleteNics = append(deleteNics, key)
		delete(existingVlanCfgs.Network.Vlans, key)

		dropin := filepath.Join(n.networkdDropinDir, fmt.Sprintf("10-netplan-%s.network.d", key))
		if utils.FileExists(dropin, utils.TypeDir) {
			// Networkd dropin is a directory for each interface with override.conf file.
			// Remove should delete the complete directory instead of that one file.
			deleteDirs = append(deleteDirs, dropin)
		}
	}

	logger.Infof("Deleting VLAN NICs: %v", deleteNics)
	// Simply removing configs on disk and reloading netplan/networkctl doesn't remove
	// existing vlan nics, it requires instance reboot or systemd-networkd restart. Instead,
	// make sure its removed by [networkctl delete <interfaces>] command.
	args := []string{"delete"}
	args = append(args, deleteNics...)
	if err := run.Quiet(ctx, "networkctl", args...); err != nil {
		return false, fmt.Errorf("networkctl %v failed with error: %w", args, err)
	}

	// If no more VLANs exist simply remove the file.
	if len(existingVlanCfgs.Network.Vlans) == 0 {
		logger.Infof("Removing %s dropin file for vlan rollback", netplanVlanDropinFile)
		if err := os.Remove(netplanVlanDropinFile); err != nil {
			return false, fmt.Errorf("unable to remove netplan vlan dropin (%s): %w", netplanVlanDropinFile, err)
		}
	} else {
		logger.Infof("Updating %s dropin file for vlan rollback", netplanVlanDropinFile)
		// Otherwise, overwrite configs to reflect expected interfaces.
		if _, err := n.write(existingVlanCfgs, netplanVlanSuffix); err != nil {
			return false, fmt.Errorf("unable to update vlan config at (%s): %w", netplanVlanDropinFile, err)
		}
	}

	logger.Infof("Removing directories %v as part of vlan rollback", deleteDirs)
	for _, dir := range deleteDirs {
		if err := os.RemoveAll(dir); err != nil {
			return false, fmt.Errorf("unable to remove directory %q: %w", dir, err)
		}
	}

	return true, nil
}

func (n *netplan) writeNetplanVLANDropin(nics *Interfaces) (bool, error) {
	dropin := netplanDropin{
		Network: netplanNetwork{
			Version: netplanConfigVersion,
			Vlans:   make(map[string]netplanVlan),
		},
	}

	for _, curr := range nics.VlanInterfaces {
		iface := n.vlanInterfaceName(curr.ParentInterfaceID, curr.Vlan)
		logger.Debugf("Adding %s(%d) to drop-in configuration.", iface, curr.Vlan)

		trueVal := true
		falseVal := false
		nv := netplanVlan{
			ID:                 curr.Vlan,
			Link:               n.ID(curr.ParentInterfaceID),
			DHCPv4:             &trueVal,
			OverrideMacAddress: curr.Mac,
			MTU:                curr.MTU,
			DHCP4Overrides:     &netplanDHCPOverrides{UseDomains: &falseVal},
			DHCP6Overrides:     &netplanDHCPOverrides{UseDomains: &falseVal},
		}

		if len(curr.IPv6) > 0 {
			nv.DHCPv6 = &trueVal
		}

		key := n.ID(iface)
		dropin.Network.Vlans[key] = nv
	}

	if len(nics.VlanInterfaces) == 0 {
		return false, nil
	}

	wrote, err := n.write(dropin, netplanVlanSuffix)
	if err != nil {
		return false, fmt.Errorf("failed to write netplan vlan drop-in config: %+v", err)
	}

	return wrote, nil
}

func (n *netplan) writeNetworkdVLANDropin(nics *Interfaces) (bool, error) {
	var reload bool

	googleIpv6Interfaces := vlanInterfaceListsIpv6(nics.VlanInterfaces)

	stat, err := os.Stat(n.networkdDropinDir)
	if err != nil {
		return false, fmt.Errorf("failed to stat systemd-networkd's drop-in root dir: %w", err)
	}

	if !stat.IsDir() {
		return false, fmt.Errorf("systemd-networkd drop-in dir(%s) is not a dir", n.networkdDropinDir)
	}

	for _, iface := range nics.VlanInterfaces {
		logger.Debugf("writing systemd-networkd drop-in config for VLAN ID: %d", iface.Vlan)

		var dhcp = "ipv4"
		if slices.Contains(googleIpv6Interfaces, iface.Vlan) {
			dhcp = "yes"
		}

		ifaceName := n.vlanInterfaceName(iface.ParentInterfaceID, iface.Vlan)
		matchID := n.ID(ifaceName)

		// Create and setup ini file.
		data := networkdNetplanDropin{
			Match: systemdMatchConfig{
				Name: matchID,
			},
			Network: systemdNetworkConfig{
				DNSDefaultRoute: false,
				DHCP:            dhcp,
			},
			DHCPv4: &systemdDHCPConfig{
				RoutesToDNS: false,
				RoutesToNTP: false,
			},
		}

		wrote, err := data.write(n, ifaceName)
		if err != nil {
			return false, fmt.Errorf("failed to write systemd drop-in config for VLAN ID(%s): %w", ifaceName, err)
		}

		if wrote {
			reload = true
		}
	}

	return reload, nil
}

// rollbackConfigs is the low level implementation for Rollback and RollbackNics interface.
// If removeVlan is true both regular nics and vlan nics are rolled back.
func (n *netplan) rollbackConfigs(ctx context.Context, nics *Interfaces, removeVlan bool) error {
	var reload bool
	interfaces, err := interfaceNames(nics.EthernetInterfaces)
	if err != nil {
		return fmt.Errorf("failed to get list of interface names: %v", err)
	}

	netplanEthernetDropinFile := n.dropinFile(netplanEthernetSuffix)
	existingEthernetCfgs := netplanDropin{}
	if utils.FileExists(netplanEthernetDropinFile, utils.TypeFile) {
		if err := readYamlFile(netplanEthernetDropinFile, &existingEthernetCfgs); err != nil {
			return fmt.Errorf("unable to read %q trying rollback configs: %w", netplanEthernetDropinFile, err)
		}
	}

	var deleteMe []string
	for _, iface := range interfaces {
		// Set networkd drop-in override file for removal.
		networkdDropinFile := n.networkdDropinFile(iface)
		deleteMe = append(deleteMe, networkdDropinFile)

		// Set netplan ethernet drop-in file for removal.
		if _, ok := existingEthernetCfgs.Network.Ethernets[iface]; ok {
			deleteMe = append(deleteMe, netplanEthernetDropinFile)
		}
	}

	if removeVlan {
		if done, err := n.rollbackVlanNics(ctx, nics); err != nil {
			logger.Debugf("Failed to remove vlan interfaces: %v", err)
		} else {
			if done {
				reload = true
			}
		}
	}

	for _, configFile := range deleteMe {
		logger.Debugf("Removing config file: %q", configFile)
		if err := os.Remove(configFile); err != nil {
			if !os.IsNotExist(err) {
				logger.Debugf("Failed to remove drop-in file(%s): %s", configFile, err)
			} else {
				logger.Debugf("No such drop-in file(%s), ignoring.", configFile)
			}
			continue
		}
		reload = true
	}

	if !reload {
		return nil
	}

	if err := n.reloadConfigs(ctx); err != nil {
		return fmt.Errorf("error reloading configs: %v", err)
	}

	return nil
}

// Rollback deletes the ethernet and VLAN interfaces netplan drop-in files.
func (n *netplan) Rollback(ctx context.Context, nics *Interfaces) error {
	logger.Infof("rolling back changes for %s", n.Name())
	return n.rollbackConfigs(ctx, nics, true)
}

// Rollback deletes the ethernet interfaces netplan drop-in files - only
// regular nics are handled.
func (n *netplan) RollbackNics(ctx context.Context, nics *Interfaces) error {
	logger.Infof("rolling back regular ethernet changes for %s", n.Name())
	return n.rollbackConfigs(ctx, nics, false)
}
