Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmd/core/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/cocoonstack/cocoon/images/cloudimg"
"github.com/cocoonstack/cocoon/images/oci"
"github.com/cocoonstack/cocoon/network"
bridgenet "github.com/cocoonstack/cocoon/network/bridge"
"github.com/cocoonstack/cocoon/network/cni"
"github.com/cocoonstack/cocoon/snapshot"
"github.com/cocoonstack/cocoon/snapshot/localfile"
Expand Down Expand Up @@ -172,6 +173,15 @@ func InitNetwork(conf *config.Config) (network.Network, error) {
return p, nil
}

// InitBridgeNetwork creates a TAP-on-bridge network provider.
func InitBridgeNetwork(conf *config.Config, bridgeDev string) (network.Network, error) {
p, err := bridgenet.New(conf, bridgeDev)
if err != nil {
return nil, fmt.Errorf("init bridge network: %w", err)
}
return p, nil
}

// InitSnapshot initializes the snapshot backend.
func InitSnapshot(conf *config.Config) (snapshot.Snapshot, error) {
s, err := localfile.New(conf)
Expand Down
2 changes: 2 additions & 0 deletions cmd/others/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

cmdcore "github.com/cocoonstack/cocoon/cmd/core"
"github.com/cocoonstack/cocoon/gc"
"github.com/cocoonstack/cocoon/network/bridge"
"github.com/cocoonstack/cocoon/version"
)

Expand Down Expand Up @@ -48,6 +49,7 @@ func (h Handler) GC(cmd *cobra.Command, _ []string) error {
hyper.RegisterGC(o)
}
netProvider.RegisterGC(o)
gc.Register(o, bridge.GCModule(conf.RootDir))
snapBackend.RegisterGC(o)
if err := o.Run(ctx); err != nil {
return err
Expand Down
4 changes: 3 additions & 1 deletion cmd/vm/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ func addVMFlags(cmd *cobra.Command) {
cmd.Flags().String("memory", "1G", "memory size") //nolint:mnd
cmd.Flags().String("storage", "10G", "COW disk size") //nolint:mnd
cmd.Flags().Int("nics", 1, "number of network interfaces (0 = no network); multiple NICs with auto IP config only works for cloudimg; OCI images auto-configure only the last NIC, others require manual setup inside the guest")
cmd.Flags().String("network", "", "CNI conflist name (empty = default)")
cmd.Flags().String("network", "", "CNI conflist name (empty = default); mutually exclusive with --bridge")
cmd.Flags().String("bridge", "", "use TAP-on-bridge instead of CNI (value is bridge device, e.g. cni0); VM gets IP via DHCP from the bridge")
cmd.Flags().Bool("windows", false, "Windows guest (UEFI boot, kvm_hyperv=on, no cidata)")
}

Expand All @@ -166,4 +167,5 @@ func addCloneFlags(cmd *cobra.Command) {
cmd.Flags().String("storage", "", "COW disk size (empty = inherit from snapshot)")
cmd.Flags().Int("nics", 0, "number of NICs (0 = inherit from snapshot)")
cmd.Flags().String("network", "", "CNI conflist name (empty = inherit from source VM)")
cmd.Flags().String("bridge", "", "use TAP-on-bridge instead of CNI (value is bridge device, e.g. cni0)")
}
62 changes: 54 additions & 8 deletions cmd/vm/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (
"github.com/spf13/cobra"

cmdcore "github.com/cocoonstack/cocoon/cmd/core"
"github.com/cocoonstack/cocoon/config"
"github.com/cocoonstack/cocoon/console"
"github.com/cocoonstack/cocoon/hypervisor"
"github.com/cocoonstack/cocoon/network"
bridgenet "github.com/cocoonstack/cocoon/network/bridge"
"github.com/cocoonstack/cocoon/types"
)

Expand All @@ -34,10 +36,8 @@ func (h Handler) Start(cmd *cobra.Command, args []string) error {
}

// Recover network for all backends before starting.
if netProvider, netErr := cmdcore.InitNetwork(conf); netErr == nil {
for hyper, refs := range routed {
h.recoverNetwork(ctx, hyper, netProvider, refs)
}
for hyper, refs := range routed {
h.recoverNetwork(ctx, conf, hyper, refs)
}

return batchRoutedCmd(ctx, "start", "started", routed, func(hyper hypervisor.Hypervisor, refs []string) ([]string, error) {
Expand Down Expand Up @@ -191,6 +191,8 @@ func (h Handler) RM(cmd *cobra.Command, args []string) error {
return fmt.Errorf("vm(s) deleted but network cleanup failed: %w", delErr)
}
}
// Also clean up bridge TAPs (no-op if none exist).
bridgenet.CleanupTAPs(allDeleted)
}

if lastErr != nil {
Expand All @@ -202,23 +204,67 @@ func (h Handler) RM(cmd *cobra.Command, args []string) error {
return nil
}

func (h Handler) recoverNetwork(ctx context.Context, hyper hypervisor.Hypervisor, net network.Network, refs []string) {
func (h Handler) recoverNetwork(ctx context.Context, conf *config.Config, hyper hypervisor.Hypervisor, refs []string) {
logger := log.WithFunc("cmd.recoverNetwork")

// Lazy-init CNI provider (may fail if not configured — OK for bridge-only setups).
var cniProvider network.Network
if p, err := cmdcore.InitNetwork(conf); err == nil {
cniProvider = p
}

// Cache bridge providers by device name to avoid redundant netlink lookups.
bridgeProviders := map[string]network.Network{}

for _, ref := range refs {
vm, err := hyper.Inspect(ctx, ref)
if err != nil || vm == nil || len(vm.NetworkConfigs) == 0 {
continue
}
if net.Verify(ctx, vm.ID) == nil {

netProvider, provErr := providerForVM(conf, cniProvider, bridgeProviders, vm.NetworkConfigs)
if provErr != nil {
logger.Warnf(ctx, "skip recovery for VM %s: %v", vm.ID, provErr)
continue
}
if netProvider.Verify(ctx, vm.ID) == nil {
continue
}
logger.Warnf(ctx, "netns missing for VM %s, recovering network", vm.ID)
if _, recoverErr := net.Config(ctx, vm.ID, len(vm.NetworkConfigs), &vm.Config, vm.NetworkConfigs...); recoverErr != nil {
logger.Warnf(ctx, "network missing for VM %s, recovering", vm.ID)
if _, recoverErr := netProvider.Config(ctx, vm.ID, len(vm.NetworkConfigs), &vm.Config, vm.NetworkConfigs...); recoverErr != nil {
logger.Warnf(ctx, "recover network for VM %s: %v (start will fail)", vm.ID, recoverErr)
}
}
}

// providerForVM selects the correct network provider based on persisted NetworkConfig.
func providerForVM(conf *config.Config, cniProvider network.Network, bridgeCache map[string]network.Network, configs []*types.NetworkConfig) (network.Network, error) {
if len(configs) == 0 {
return nil, fmt.Errorf("no network configs")
}
// All NICs on a VM share the same backend.
cfg := configs[0]
if cfg.Backend == "bridge" {
if cfg.BridgeDev == "" {
return nil, fmt.Errorf("bridge backend but no bridge device persisted")
}
if cached, ok := bridgeCache[cfg.BridgeDev]; ok {
return cached, nil
}
p, err := cmdcore.InitBridgeNetwork(conf, cfg.BridgeDev)
if err != nil {
return nil, err
}
bridgeCache[cfg.BridgeDev] = p
return p, nil
}
// "cni" or empty (backward compat).
if cniProvider == nil {
return nil, fmt.Errorf("CNI provider not available")
}
return cniProvider, nil
}

// batchRoutedCmd runs a batch operation across multiple backends.
func batchRoutedCmd(ctx context.Context, name, pastTense string, routed map[hypervisor.Hypervisor][]string, fn func(hypervisor.Hypervisor, []string) ([]string, error)) error {
logger := log.WithFunc("cmd." + name)
Expand Down
19 changes: 15 additions & 4 deletions cmd/vm/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ func (h Handler) prepareClone(ctx context.Context, cmd *cobra.Command, conf *con
return nil, "", nil, nil, fmt.Errorf("--nics %d below snapshot minimum %d", nics, cfg.NICs)
}

netProvider, networkConfigs, err := initNetwork(ctx, conf, vmID, nics, vmCfg, tapQueues(vmCfg.CPU, conf.UseFirecracker))
bridgeDev, _ := cmd.Flags().GetString("bridge")
netProvider, networkConfigs, err := initNetwork(ctx, conf, vmID, nics, vmCfg, tapQueues(vmCfg.CPU, conf.UseFirecracker), bridgeDev)
if err != nil {
return nil, "", nil, nil, err
}
Expand Down Expand Up @@ -288,6 +289,10 @@ func (h Handler) createVM(cmd *cobra.Command, image string) (context.Context, *t
if conf.UseFirecracker && vmCfg.Windows {
return nil, nil, nil, fmt.Errorf("--fc and --windows are mutually exclusive: Firecracker does not support Windows guests")
}
bridgeDev, _ := cmd.Flags().GetString("bridge")
if bridgeDev != "" && vmCfg.Network != "" {
return nil, nil, nil, fmt.Errorf("--bridge and --network are mutually exclusive")
}

backends, hyper, err := cmdcore.InitBackends(ctx, conf)
if err != nil {
Expand All @@ -312,7 +317,7 @@ func (h Handler) createVM(cmd *cobra.Command, image string) (context.Context, *t
}

nics, _ := cmd.Flags().GetInt("nics")
netProvider, networkConfigs, err := initNetwork(ctx, conf, vmID, nics, vmCfg, tapQueues(vmCfg.CPU, conf.UseFirecracker))
netProvider, networkConfigs, err := initNetwork(ctx, conf, vmID, nics, vmCfg, tapQueues(vmCfg.CPU, conf.UseFirecracker), bridgeDev)
if err != nil {
return nil, nil, nil, err
}
Expand All @@ -334,11 +339,17 @@ func tapQueues(cpu int, useFC bool) int {
return cpu
}

func initNetwork(ctx context.Context, conf *config.Config, vmID string, nics int, vmCfg *types.VMConfig, queues int) (network.Network, []*types.NetworkConfig, error) {
func initNetwork(ctx context.Context, conf *config.Config, vmID string, nics int, vmCfg *types.VMConfig, queues int, bridgeDev string) (network.Network, []*types.NetworkConfig, error) {
if nics <= 0 {
return nil, nil, nil
}
netProvider, err := cmdcore.InitNetwork(conf)
var netProvider network.Network
var err error
if bridgeDev != "" {
netProvider, err = cmdcore.InitBridgeNetwork(conf, bridgeDev)
} else {
netProvider, err = cmdcore.InitNetwork(conf)
}
if err != nil {
return nil, nil, fmt.Errorf("init network: %w", err)
}
Expand Down
6 changes: 3 additions & 3 deletions hypervisor/cloudhypervisor/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ func (ch *CloudHypervisor) launchProcess(ctx context.Context, rec *hypervisor.VM
cmd.Stderr = logFile
}

// If the VM has network, CH must be launched inside the VM's netns
// so it can access the tap device. We setns before fork and restore after.
if withNetwork {
// CNI mode: TAP is inside a per-VM netns, switch before fork.
// Bridge mode: TAP is in host netns, no EnterNetns needed.
if withNetwork && rec.NetworkConfigs[0].NetnsPath != "" {
restore, enterErr := hypervisor.EnterNetns(rec.NetworkConfigs[0].NetnsPath)
if enterErr != nil {
return 0, fmt.Errorf("enter netns: %w", enterErr)
Expand Down
2 changes: 1 addition & 1 deletion hypervisor/firecracker/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (fc *Firecracker) launchProcess(ctx context.Context, rec *hypervisor.VMReco
fcCmd.Stdin = slave
fcCmd.Stdout = slave

if withNetwork {
if withNetwork && rec.NetworkConfigs[0].NetnsPath != "" {
restore, enterErr := hypervisor.EnterNetns(rec.NetworkConfigs[0].NetnsPath)
if enterErr != nil {
_ = master.Close()
Expand Down
Loading
Loading